@burdenoff/fluidgrids-fe-sdk 2026.1031.7 → 2026.1101.2
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/dist/{ExecutionDetailPage-4LPCNWM3.js → ExecutionDetailPage-Q77U6YJO.js} +11 -8
- package/dist/{ExecutionsListPage-HNPUKYD7.js → ExecutionsListPage-X4H4FVLN.js} +6 -5
- package/dist/{WorkflowBuilder-QFSCYCUC.js → WorkflowBuilder-RJPRUMQR.js} +11 -8
- package/dist/{WorkflowsList-RV4BRJO7.js → WorkflowsList-R2JJK7BL.js} +7 -6
- package/dist/agentTools-MISTH7JI.js +349 -0
- package/dist/{chunk-77HIU3CX.js → chunk-26HGYPBC.js} +1 -1
- package/dist/{chunk-76UUEMTN.js → chunk-37VUN74D.js} +4 -4
- package/dist/{chunk-QPZCJKII.js → chunk-3ERT6FQK.js} +3 -3
- package/dist/chunk-472HJ3S3.js +3929 -0
- package/dist/{chunk-WNZV4SLU.js → chunk-4QHCXTHW.js} +4 -4
- package/dist/chunk-AASHQI77.js +126 -0
- package/dist/chunk-CGXQVZGY.js +260 -0
- package/dist/chunk-CRDG2SQQ.js +185 -0
- package/dist/chunk-DQ2ZCXYX.js +21875 -0
- package/dist/chunk-F7DD7BOB.js +1164 -0
- package/dist/chunk-GTEBQQ7J.js +24 -0
- package/dist/{chunk-ALFMJSD6.js → chunk-H7XPXONT.js} +1 -1
- package/dist/{chunk-UO5DUKYN.js → chunk-HKKSXOFO.js} +8 -7
- package/dist/chunk-IA2ZCHNQ.js +1336 -0
- package/dist/{chunk-3M36XWAH.js → chunk-IGRIN7NL.js} +8 -7
- package/dist/chunk-JDLQ6QIB.js +283 -0
- package/dist/{chunk-AD7BICOT.js → chunk-JKBCLBHY.js} +1 -1
- package/dist/{chunk-6JX5UPDW.js → chunk-JNZCXFLD.js} +1 -1
- package/dist/{chunk-SEFOT2LD.js → chunk-LWLHPVIA.js} +1 -1
- package/dist/{chunk-JWROEOG5.js → chunk-LYTR6IQO.js} +1 -38
- package/dist/chunk-MR2B67NW.js +338 -0
- package/dist/{chunk-RIHYPPMQ.js → chunk-NLYQ3XDE.js} +1 -1
- package/dist/{chunk-6ILIBJFH.js → chunk-NZJWKVER.js} +7904 -7943
- package/dist/chunk-OCJTTHTS.js +46 -0
- package/dist/chunk-OSQ3OCR3.js +1193 -0
- package/dist/{chunk-VJL3DFJT.js → chunk-REO4S5MY.js} +2 -2
- package/dist/{chunk-XKQMCPSB.js → chunk-T2UUYYVJ.js} +4 -4
- package/dist/{chunk-BXR6BWXM.js → chunk-W7MTSATB.js} +5 -5
- package/dist/{chunk-A47PSD7A.js → chunk-WF6GMBR4.js} +7 -4
- package/dist/chunk-WYCB6OQL.js +19709 -0
- package/dist/{chunk-YOLPHVOE.js → chunk-YKFR7BZJ.js} +1 -1
- package/dist/{chunk-26NENGXA.js → chunk-YSXNJGCY.js} +4 -4
- package/dist/code-editor-field-JKCUR6BI.js +3 -0
- package/dist/executions.js +12 -9
- package/dist/history.js +4 -3
- package/dist/index.js +41 -32
- package/dist/node-detail-view-PCYFQX62.js +2034 -0
- package/dist/observability.js +5 -4
- package/dist/quick-config-panel-BRZZHOSI.js +402 -0
- package/dist/settings.js +6 -5
- package/dist/templates.js +10 -7
- package/dist/workflow-builder.js +12 -9
- package/dist/workflows-list.js +7 -6
- package/package.json +4 -1
- package/dist/chunk-NMEI7TQW.js +0 -2628
- package/dist/chunk-S7XV65DB.js +0 -50350
package/dist/chunk-NMEI7TQW.js
DELETED
|
@@ -1,2628 +0,0 @@
|
|
|
1
|
-
import { useEntityTags, useTagEntity, useUntagEntity } from './chunk-PZ45H72P.js';
|
|
2
|
-
import { notifyFluidGridsAiCreditsUpdated } from './chunk-Y53CVZVV.js';
|
|
3
|
-
import { ErrorType } from './chunk-7KKYIRLH.js';
|
|
4
|
-
import { throwIfResultError, describeOperationError, signalPayloadCard, components_default, isForbiddenError, payloadFromCardValues, parseRawSignalPayload } from './chunk-6ILIBJFH.js';
|
|
5
|
-
import { getFluidGridsConfig } from './chunk-6AMNEX2Y.js';
|
|
6
|
-
import { TerminateWorkflowRunDocument, WorkflowRunHistoryDocument, RunEventAppendedDocument, WorkflowProposalDocument, ProposeRunFixDocument, ProposeWorkflowChangeDocument, ApplyWorkflowProposalDocument, RejectWorkflowProposalDocument, CancelWorkflowProposalDocument, WorkflowTestCasesDocument, WorkflowTestRunsDocument, WorkflowTestRunDocument, CreateWorkflowTestCaseDocument, UpdateWorkflowTestCaseDocument, DeleteWorkflowTestCaseDocument, RunWorkflowTestsDocument, CreateWorkflowTestCaseFromRunDocument, DurableRunFieldsDocument, WorkflowRunDeterminismStatusDocument, WorkflowRunStackTraceDocument, SignalWorkflowRunDocument } from './chunk-IHEV5HTD.js';
|
|
7
|
-
import { useTr } from './chunk-UULVIT25.js';
|
|
8
|
-
import { getFluidGridsClient, getFluidGridsClientSafe } from './chunk-NTWG3CJX.js';
|
|
9
|
-
import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
10
|
-
import { TagBadge, TagsWidget } from '@burdenoff/fe-libs/tag';
|
|
11
|
-
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
12
|
-
import { Icon } from '@iconify/react';
|
|
13
|
-
import { Button } from '@burdenoff/fe-libs/button';
|
|
14
|
-
import { cn } from '@burdenoff/fe-libs/utils';
|
|
15
|
-
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Label, Textarea, DialogFooter, Badge, Button as Button$1, Input } from '@burdenoff/fe-libs/ui';
|
|
16
|
-
|
|
17
|
-
function toTagItem(assignment) {
|
|
18
|
-
return {
|
|
19
|
-
id: assignment.tagId,
|
|
20
|
-
key: assignment.tagInfo?.key ?? assignment.tagId,
|
|
21
|
-
label: assignment.tagInfo?.label ?? assignment.tagInfo?.key ?? "Tag",
|
|
22
|
-
color: assignment.tagInfo?.color ?? null
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
function EntityTagPicker({
|
|
26
|
-
entityType,
|
|
27
|
-
entityId,
|
|
28
|
-
onDraftChange,
|
|
29
|
-
readOnly = false,
|
|
30
|
-
onChange,
|
|
31
|
-
className
|
|
32
|
-
}) {
|
|
33
|
-
const draftMode = !entityId;
|
|
34
|
-
const { workspaceId } = getFluidGridsConfig();
|
|
35
|
-
const { tags: assignments, refetch } = useEntityTags(entityType, entityId);
|
|
36
|
-
const tagEntity = useTagEntity();
|
|
37
|
-
const untagEntity = useUntagEntity();
|
|
38
|
-
const [current, setCurrent] = useState([]);
|
|
39
|
-
const currentRef = useRef([]);
|
|
40
|
-
const assignmentKey = assignments.map((a) => a.tagId).join("|");
|
|
41
|
-
const syncedKey = useRef(null);
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
if (draftMode || syncedKey.current === assignmentKey) return;
|
|
44
|
-
syncedKey.current = assignmentKey;
|
|
45
|
-
const synced = assignments.map(toTagItem);
|
|
46
|
-
currentRef.current = synced;
|
|
47
|
-
setCurrent(synced);
|
|
48
|
-
}, [assignmentKey, assignments, draftMode]);
|
|
49
|
-
const handleAdd = useCallback(
|
|
50
|
-
async (tag) => {
|
|
51
|
-
if (currentRef.current.some((t) => t.id === tag.id)) return;
|
|
52
|
-
const next = [...currentRef.current, tag];
|
|
53
|
-
currentRef.current = next;
|
|
54
|
-
setCurrent(next);
|
|
55
|
-
if (draftMode) {
|
|
56
|
-
onDraftChange?.(next.map((t) => t.id));
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
await tagEntity(entityType, entityId, tag.id);
|
|
61
|
-
await refetch();
|
|
62
|
-
onChange?.();
|
|
63
|
-
} catch {
|
|
64
|
-
const rolledBack = currentRef.current.filter((t) => t.id !== tag.id);
|
|
65
|
-
currentRef.current = rolledBack;
|
|
66
|
-
setCurrent(rolledBack);
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
[
|
|
70
|
-
draftMode,
|
|
71
|
-
entityId,
|
|
72
|
-
entityType,
|
|
73
|
-
onChange,
|
|
74
|
-
onDraftChange,
|
|
75
|
-
refetch,
|
|
76
|
-
tagEntity
|
|
77
|
-
]
|
|
78
|
-
);
|
|
79
|
-
const handleRemove = useCallback(
|
|
80
|
-
async (tagId) => {
|
|
81
|
-
const removed = currentRef.current.find((t) => t.id === tagId);
|
|
82
|
-
const next = currentRef.current.filter((t) => t.id !== tagId);
|
|
83
|
-
currentRef.current = next;
|
|
84
|
-
setCurrent(next);
|
|
85
|
-
if (draftMode) {
|
|
86
|
-
onDraftChange?.(next.map((t) => t.id));
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
await untagEntity(entityType, entityId, tagId);
|
|
91
|
-
await refetch();
|
|
92
|
-
onChange?.();
|
|
93
|
-
} catch {
|
|
94
|
-
if (removed) {
|
|
95
|
-
const rolledBack = [...currentRef.current, removed];
|
|
96
|
-
currentRef.current = rolledBack;
|
|
97
|
-
setCurrent(rolledBack);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
[
|
|
102
|
-
draftMode,
|
|
103
|
-
entityId,
|
|
104
|
-
entityType,
|
|
105
|
-
onChange,
|
|
106
|
-
onDraftChange,
|
|
107
|
-
refetch,
|
|
108
|
-
untagEntity
|
|
109
|
-
]
|
|
110
|
-
);
|
|
111
|
-
if (readOnly) {
|
|
112
|
-
if (current.length === 0) return null;
|
|
113
|
-
return /* @__PURE__ */ jsx("div", { className: `flex flex-wrap items-center gap-1 ${className ?? ""}`, children: current.map((tag) => /* @__PURE__ */ jsx(
|
|
114
|
-
TagBadge,
|
|
115
|
-
{
|
|
116
|
-
id: tag.id,
|
|
117
|
-
label: tag.label,
|
|
118
|
-
color: tag.color ?? void 0,
|
|
119
|
-
size: "sm"
|
|
120
|
-
},
|
|
121
|
-
tag.id
|
|
122
|
-
)) });
|
|
123
|
-
}
|
|
124
|
-
return /* @__PURE__ */ jsx(
|
|
125
|
-
TagsWidget,
|
|
126
|
-
{
|
|
127
|
-
entityType,
|
|
128
|
-
entityId: entityId ?? "",
|
|
129
|
-
workspaceId: workspaceId || void 0,
|
|
130
|
-
tags: current,
|
|
131
|
-
onAdd: handleAdd,
|
|
132
|
-
onRemove: handleRemove,
|
|
133
|
-
syncGraphNotifications: !draftMode,
|
|
134
|
-
className
|
|
135
|
-
}
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// src/modules/executions/utils/run-history.ts
|
|
140
|
-
function isRecord(value) {
|
|
141
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
142
|
-
}
|
|
143
|
-
function str(value) {
|
|
144
|
-
return typeof value === "string" && value.trim() ? value : void 0;
|
|
145
|
-
}
|
|
146
|
-
function num(value) {
|
|
147
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
148
|
-
}
|
|
149
|
-
function payloadOf(event) {
|
|
150
|
-
return isRecord(event.payload) ? event.payload : {};
|
|
151
|
-
}
|
|
152
|
-
function mergeHistoryEvents(current, incoming) {
|
|
153
|
-
if (incoming.length === 0) return current;
|
|
154
|
-
const seen = new Set(current.map((event) => event.seq));
|
|
155
|
-
const fresh = incoming.filter((event) => {
|
|
156
|
-
if (seen.has(event.seq)) return false;
|
|
157
|
-
seen.add(event.seq);
|
|
158
|
-
return true;
|
|
159
|
-
});
|
|
160
|
-
if (fresh.length === 0) return current;
|
|
161
|
-
return [...current, ...fresh].sort((a, b) => a.seq - b.seq);
|
|
162
|
-
}
|
|
163
|
-
function lastSeqOf(events) {
|
|
164
|
-
return events.length ? events[events.length - 1].seq : 0;
|
|
165
|
-
}
|
|
166
|
-
function formatDurationMs(ms) {
|
|
167
|
-
if (ms < 1e3) return `${Math.max(0, Math.round(ms))}ms`;
|
|
168
|
-
const seconds = Math.round(ms / 1e3);
|
|
169
|
-
if (seconds < 60) return `${seconds}s`;
|
|
170
|
-
const minutes = Math.floor(seconds / 60);
|
|
171
|
-
if (minutes < 60) {
|
|
172
|
-
const rest2 = seconds % 60;
|
|
173
|
-
return rest2 ? `${minutes}m ${rest2}s` : `${minutes}m`;
|
|
174
|
-
}
|
|
175
|
-
const hours = Math.floor(minutes / 60);
|
|
176
|
-
if (hours < 24) {
|
|
177
|
-
const rest2 = minutes % 60;
|
|
178
|
-
return rest2 ? `${hours}h ${rest2}m` : `${hours}h`;
|
|
179
|
-
}
|
|
180
|
-
const days = Math.floor(hours / 24);
|
|
181
|
-
const rest = hours % 24;
|
|
182
|
-
return rest ? `${days}d ${rest}h` : `${days}d`;
|
|
183
|
-
}
|
|
184
|
-
function failureOf(value) {
|
|
185
|
-
if (!isRecord(value)) return void 0;
|
|
186
|
-
const message = str(value.message);
|
|
187
|
-
if (!message) return void 0;
|
|
188
|
-
return {
|
|
189
|
-
message,
|
|
190
|
-
errorType: str(value.errorType) ?? str(value.type),
|
|
191
|
-
...value.category === "BENIGN" ? { benign: true } : {}
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
function groupKeyOf(event) {
|
|
195
|
-
const payload = payloadOf(event);
|
|
196
|
-
const type = event.type;
|
|
197
|
-
if (type.startsWith("WORKFLOW_TASK_")) {
|
|
198
|
-
return ["task", str(payload.taskId) ?? str(event.taskId) ?? `${event.seq}`];
|
|
199
|
-
}
|
|
200
|
-
if (type.startsWith("ACTIVITY_")) {
|
|
201
|
-
return ["activity", str(payload.activityId) ?? `${event.seq}`];
|
|
202
|
-
}
|
|
203
|
-
if (type.startsWith("LOCAL_ACTIVITY_")) {
|
|
204
|
-
return ["activity", str(payload.localActivityId) ?? `${event.seq}`];
|
|
205
|
-
}
|
|
206
|
-
if (type.startsWith("TIMER_")) {
|
|
207
|
-
return ["timer", str(payload.timerId) ?? `${event.seq}`];
|
|
208
|
-
}
|
|
209
|
-
if (type.startsWith("CHILD_RUN_")) {
|
|
210
|
-
return ["child", str(payload.childId) ?? `${event.seq}`];
|
|
211
|
-
}
|
|
212
|
-
if (type.startsWith("EXTERNAL_RUN_")) {
|
|
213
|
-
return ["external", str(payload.externalId) ?? `${event.seq}`];
|
|
214
|
-
}
|
|
215
|
-
if (type.startsWith("UPDATE_")) {
|
|
216
|
-
return ["update", str(payload.updateId) ?? `${event.seq}`];
|
|
217
|
-
}
|
|
218
|
-
if (type === "SIGNAL_RECEIVED") return ["signal", `${event.seq}`];
|
|
219
|
-
if (type === "MARKER_RECORDED") return ["marker", `${event.seq}`];
|
|
220
|
-
if (type === "SIDE_EFFECT_RECORDED") return ["sideEffect", `${event.seq}`];
|
|
221
|
-
if (type === "CONTEXT_UPSERTED") return ["context", `${event.seq}`];
|
|
222
|
-
if (type.startsWith("WORKFLOW_")) return ["workflow", `${event.seq}`];
|
|
223
|
-
return ["other", `${event.seq}`];
|
|
224
|
-
}
|
|
225
|
-
var ACTIVITY_STATUS = {
|
|
226
|
-
ACTIVITY_SCHEDULED: "scheduled",
|
|
227
|
-
ACTIVITY_STARTED: "running",
|
|
228
|
-
ACTIVITY_COMPLETED: "completed",
|
|
229
|
-
ACTIVITY_FAILED: "failed",
|
|
230
|
-
ACTIVITY_TIMED_OUT: "timedOut",
|
|
231
|
-
ACTIVITY_CANCEL_REQUESTED: "cancelRequested",
|
|
232
|
-
ACTIVITY_CANCELLED: "cancelled",
|
|
233
|
-
LOCAL_ACTIVITY_COMPLETED: "completed",
|
|
234
|
-
LOCAL_ACTIVITY_FAILED: "failed"
|
|
235
|
-
};
|
|
236
|
-
var TASK_STATUS = {
|
|
237
|
-
WORKFLOW_TASK_SCHEDULED: "scheduled",
|
|
238
|
-
WORKFLOW_TASK_STARTED: "running",
|
|
239
|
-
WORKFLOW_TASK_COMPLETED: "completed",
|
|
240
|
-
WORKFLOW_TASK_FAILED: "failed",
|
|
241
|
-
WORKFLOW_TASK_TIMED_OUT: "timedOut"
|
|
242
|
-
};
|
|
243
|
-
var TIMER_STATUS = {
|
|
244
|
-
TIMER_STARTED: "running",
|
|
245
|
-
TIMER_FIRED: "fired",
|
|
246
|
-
TIMER_CANCELLED: "cancelled"
|
|
247
|
-
};
|
|
248
|
-
var CHILD_STATUS = {
|
|
249
|
-
CHILD_RUN_INITIATED: "scheduled",
|
|
250
|
-
CHILD_RUN_STARTED: "running",
|
|
251
|
-
CHILD_RUN_COMPLETED: "completed",
|
|
252
|
-
CHILD_RUN_FAILED: "failed",
|
|
253
|
-
CHILD_RUN_CANCELLED: "cancelled",
|
|
254
|
-
CHILD_RUN_TERMINATED: "terminated",
|
|
255
|
-
CHILD_RUN_TIMED_OUT: "timedOut"
|
|
256
|
-
};
|
|
257
|
-
var WORKFLOW_STATUS = {
|
|
258
|
-
WORKFLOW_STARTED: "running",
|
|
259
|
-
WORKFLOW_COMPLETED: "completed",
|
|
260
|
-
WORKFLOW_FAILED: "failed",
|
|
261
|
-
WORKFLOW_CANCELLED: "cancelled",
|
|
262
|
-
WORKFLOW_CANCEL_REQUESTED: "cancelRequested",
|
|
263
|
-
WORKFLOW_TERMINATED: "terminated",
|
|
264
|
-
WORKFLOW_TIMED_OUT: "timedOut",
|
|
265
|
-
WORKFLOW_CONTINUED_AS_NEW: "continued"
|
|
266
|
-
};
|
|
267
|
-
var EXTERNAL_STATUS = {
|
|
268
|
-
EXTERNAL_RUN_CANCEL_REQUESTED: "cancelRequested",
|
|
269
|
-
EXTERNAL_RUN_CANCEL_RESOLVED: "completed",
|
|
270
|
-
EXTERNAL_RUN_SIGNALED: "completed"
|
|
271
|
-
};
|
|
272
|
-
var UPDATE_STATUS = {
|
|
273
|
-
UPDATE_ADMITTED: "scheduled",
|
|
274
|
-
UPDATE_ACCEPTED: "running",
|
|
275
|
-
UPDATE_COMPLETED: "completed"
|
|
276
|
-
};
|
|
277
|
-
var FINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
278
|
-
"completed",
|
|
279
|
-
"failed",
|
|
280
|
-
"timedOut",
|
|
281
|
-
"cancelled",
|
|
282
|
-
"terminated",
|
|
283
|
-
"continued",
|
|
284
|
-
"fired",
|
|
285
|
-
"received",
|
|
286
|
-
"recorded"
|
|
287
|
-
]);
|
|
288
|
-
function statusFor(kind, type) {
|
|
289
|
-
switch (kind) {
|
|
290
|
-
case "activity":
|
|
291
|
-
return ACTIVITY_STATUS[type];
|
|
292
|
-
case "task":
|
|
293
|
-
return TASK_STATUS[type];
|
|
294
|
-
case "timer":
|
|
295
|
-
return TIMER_STATUS[type];
|
|
296
|
-
case "child":
|
|
297
|
-
return CHILD_STATUS[type];
|
|
298
|
-
case "workflow":
|
|
299
|
-
return WORKFLOW_STATUS[type];
|
|
300
|
-
case "external":
|
|
301
|
-
return EXTERNAL_STATUS[type];
|
|
302
|
-
case "update":
|
|
303
|
-
return UPDATE_STATUS[type];
|
|
304
|
-
case "signal":
|
|
305
|
-
return "received";
|
|
306
|
-
default:
|
|
307
|
-
return "recorded";
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
var WORKFLOW_LABELS = {
|
|
311
|
-
WORKFLOW_STARTED: "Workflow started",
|
|
312
|
-
WORKFLOW_COMPLETED: "Workflow completed",
|
|
313
|
-
WORKFLOW_FAILED: "Workflow failed",
|
|
314
|
-
WORKFLOW_CANCELLED: "Workflow cancelled",
|
|
315
|
-
WORKFLOW_CANCEL_REQUESTED: "Cancel requested",
|
|
316
|
-
WORKFLOW_TERMINATED: "Workflow terminated",
|
|
317
|
-
WORKFLOW_TIMED_OUT: "Workflow timed out",
|
|
318
|
-
WORKFLOW_CONTINUED_AS_NEW: "Continued as new",
|
|
319
|
-
WORKFLOW_RESET: "Workflow reset"
|
|
320
|
-
};
|
|
321
|
-
function humanizeEventType(type) {
|
|
322
|
-
const words = type.toLowerCase().split("_").filter(Boolean);
|
|
323
|
-
if (!words.length) return type;
|
|
324
|
-
const first = words[0];
|
|
325
|
-
return [first.charAt(0).toUpperCase() + first.slice(1), ...words.slice(1)].join(" ").trim();
|
|
326
|
-
}
|
|
327
|
-
function describe(kind, id, first) {
|
|
328
|
-
const payload = payloadOf(first);
|
|
329
|
-
const summary = str(payload.summary);
|
|
330
|
-
switch (kind) {
|
|
331
|
-
case "activity": {
|
|
332
|
-
const nodeType = str(payload.nodeType);
|
|
333
|
-
const operation = str(payload.operation);
|
|
334
|
-
const target = [nodeType, operation].filter(Boolean).join(" \xB7 ");
|
|
335
|
-
return {
|
|
336
|
-
label: summary ?? (target || `Activity ${id}`),
|
|
337
|
-
detail: summary && target ? target : id
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
case "timer": {
|
|
341
|
-
const duration = num(payload.durationMs);
|
|
342
|
-
const span = duration !== void 0 ? formatDurationMs(duration) : "";
|
|
343
|
-
return {
|
|
344
|
-
label: summary ?? (span ? `Timer ${span}` : `Timer ${id}`),
|
|
345
|
-
detail: summary && span ? span : id
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
case "child": {
|
|
349
|
-
const target = str(payload.workflowFn) ?? str(payload.workflowKey);
|
|
350
|
-
return {
|
|
351
|
-
label: summary ?? (target ? `Child ${target}` : `Child run ${id}`),
|
|
352
|
-
detail: id
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
case "signal":
|
|
356
|
-
return { label: `Signal ${str(payload.name) ?? ""}`.trim() };
|
|
357
|
-
case "task":
|
|
358
|
-
return { label: "Workflow task", detail: id };
|
|
359
|
-
case "external": {
|
|
360
|
-
const name = str(payload.name);
|
|
361
|
-
return {
|
|
362
|
-
label: name ? `Signal ${name} to another run` : first.type === "EXTERNAL_RUN_CANCEL_REQUESTED" ? "Cancel another run" : humanizeEventType(first.type),
|
|
363
|
-
detail: id
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
case "marker": {
|
|
367
|
-
const markerId = str(payload.markerId) ?? "";
|
|
368
|
-
return {
|
|
369
|
-
label: markerId.startsWith("patch:") ? `Patch ${markerId.slice("patch:".length)}` : "Marker",
|
|
370
|
-
detail: markerId || void 0
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
case "sideEffect":
|
|
374
|
-
return { label: "Side effect", detail: str(payload.sideEffectId) };
|
|
375
|
-
case "context":
|
|
376
|
-
return { label: "Context updated" };
|
|
377
|
-
case "update":
|
|
378
|
-
return { label: `Update ${str(payload.name) ?? id}` };
|
|
379
|
-
case "workflow":
|
|
380
|
-
return {
|
|
381
|
-
label: WORKFLOW_LABELS[first.type] ?? humanizeEventType(first.type)
|
|
382
|
-
};
|
|
383
|
-
default:
|
|
384
|
-
return { label: humanizeEventType(first.type) };
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
function groupRunHistory(events) {
|
|
388
|
-
const groups = /* @__PURE__ */ new Map();
|
|
389
|
-
const ordered = [...events].sort((a, b) => a.seq - b.seq);
|
|
390
|
-
for (const event of ordered) {
|
|
391
|
-
const [kind, id] = groupKeyOf(event);
|
|
392
|
-
const key = `${kind}:${id}`;
|
|
393
|
-
const payload = payloadOf(event);
|
|
394
|
-
let group = groups.get(key);
|
|
395
|
-
if (!group) {
|
|
396
|
-
const { label, detail } = describe(kind, id, event);
|
|
397
|
-
group = {
|
|
398
|
-
key,
|
|
399
|
-
kind,
|
|
400
|
-
id,
|
|
401
|
-
label,
|
|
402
|
-
detail,
|
|
403
|
-
status: statusFor(kind, event.type) ?? "recorded",
|
|
404
|
-
events: [],
|
|
405
|
-
firstSeq: event.seq,
|
|
406
|
-
lastSeq: event.seq,
|
|
407
|
-
startedAt: event.createdAt
|
|
408
|
-
};
|
|
409
|
-
groups.set(key, group);
|
|
410
|
-
}
|
|
411
|
-
group.events.push(event);
|
|
412
|
-
group.lastSeq = event.seq;
|
|
413
|
-
const status = statusFor(kind, event.type);
|
|
414
|
-
if (status && !(status === "cancelRequested" && FINAL_STATUSES.has(group.status))) {
|
|
415
|
-
group.status = status;
|
|
416
|
-
}
|
|
417
|
-
if (FINAL_STATUSES.has(group.status)) group.endedAt = event.createdAt;
|
|
418
|
-
const attempt = num(payload.attempt);
|
|
419
|
-
if (attempt !== void 0)
|
|
420
|
-
group.attempt = Math.max(group.attempt ?? 0, attempt);
|
|
421
|
-
const failure = failureOf(payload.failure) ?? (event.type === "WORKFLOW_TASK_FAILED" || event.type === "WORKFLOW_TASK_TIMED_OUT" ? failureOf({ message: payload.message, errorType: payload.cause }) : void 0);
|
|
422
|
-
if (failure) {
|
|
423
|
-
group.failure = {
|
|
424
|
-
...failure,
|
|
425
|
-
cause: str(payload.cause)
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
const childRunId = str(payload.childRunId);
|
|
429
|
-
if (kind === "child" && childRunId) group.childRunId = childRunId;
|
|
430
|
-
}
|
|
431
|
-
return [...groups.values()].sort((a, b) => a.firstSeq - b.firstSeq);
|
|
432
|
-
}
|
|
433
|
-
var KIND_ICONS = {
|
|
434
|
-
workflow: "mdi:flag-outline",
|
|
435
|
-
task: "mdi:cog-sync-outline",
|
|
436
|
-
activity: "mdi:lightning-bolt-outline",
|
|
437
|
-
timer: "mdi:timer-sand",
|
|
438
|
-
child: "mdi:source-branch",
|
|
439
|
-
signal: "mdi:bell-ring-outline",
|
|
440
|
-
external: "mdi:send-outline",
|
|
441
|
-
marker: "mdi:bookmark-outline",
|
|
442
|
-
sideEffect: "mdi:dice-multiple-outline",
|
|
443
|
-
context: "mdi:tag-text-outline",
|
|
444
|
-
update: "mdi:pencil-outline",
|
|
445
|
-
other: "mdi:circle-small"
|
|
446
|
-
};
|
|
447
|
-
var STATUS_CLASS = {
|
|
448
|
-
scheduled: "bg-background-tertiary text-text-secondary border-border-secondary",
|
|
449
|
-
running: "bg-[var(--color-status-info-bg-subtle)] text-[var(--color-status-info-text)] border-[var(--color-status-info-border)]",
|
|
450
|
-
completed: "bg-[var(--color-status-success-bg-subtle)] text-[var(--color-status-success-text)] border-[var(--color-status-success-border)]",
|
|
451
|
-
fired: "bg-[var(--color-status-success-bg-subtle)] text-[var(--color-status-success-text)] border-[var(--color-status-success-border)]",
|
|
452
|
-
received: "bg-[var(--color-status-success-bg-subtle)] text-[var(--color-status-success-text)] border-[var(--color-status-success-border)]",
|
|
453
|
-
recorded: "bg-background-tertiary text-text-secondary border-border-secondary",
|
|
454
|
-
failed: "bg-[var(--color-status-error-bg-subtle)] text-[var(--color-status-error-text)] border-[var(--color-status-error-border)]",
|
|
455
|
-
timedOut: "bg-[var(--color-status-error-bg-subtle)] text-[var(--color-status-error-text)] border-[var(--color-status-error-border)]",
|
|
456
|
-
terminated: "bg-[var(--color-status-error-bg-subtle)] text-[var(--color-status-error-text)] border-[var(--color-status-error-border)]",
|
|
457
|
-
cancelRequested: "bg-[var(--color-status-warning-bg-subtle)] text-[var(--color-status-warning-text)] border-[var(--color-status-warning-border)]",
|
|
458
|
-
cancelled: "bg-[var(--color-status-warning-bg-subtle)] text-[var(--color-status-warning-text)] border-[var(--color-status-warning-border)]",
|
|
459
|
-
continued: "bg-[var(--color-status-info-bg-subtle)] text-[var(--color-status-info-text)] border-[var(--color-status-info-border)]"
|
|
460
|
-
};
|
|
461
|
-
var BENIGN_CLASS = "bg-background-tertiary text-text-secondary border-border-secondary";
|
|
462
|
-
var STATUS_LABEL = {
|
|
463
|
-
scheduled: ["fluidgrids.executions.history.status.scheduled", "Scheduled"],
|
|
464
|
-
running: ["fluidgrids.executions.history.status.running", "Running"],
|
|
465
|
-
completed: ["fluidgrids.executions.history.status.completed", "Completed"],
|
|
466
|
-
failed: ["fluidgrids.executions.history.status.failed", "Failed"],
|
|
467
|
-
timedOut: ["fluidgrids.executions.history.status.timedOut", "Timed out"],
|
|
468
|
-
cancelled: ["fluidgrids.executions.history.status.cancelled", "Cancelled"],
|
|
469
|
-
cancelRequested: [
|
|
470
|
-
"fluidgrids.executions.history.status.cancelRequested",
|
|
471
|
-
"Cancel requested"
|
|
472
|
-
],
|
|
473
|
-
terminated: ["fluidgrids.executions.history.status.terminated", "Terminated"],
|
|
474
|
-
continued: ["fluidgrids.executions.history.status.continued", "Continued"],
|
|
475
|
-
fired: ["fluidgrids.executions.history.status.fired", "Fired"],
|
|
476
|
-
received: ["fluidgrids.executions.history.status.received", "Received"],
|
|
477
|
-
recorded: ["fluidgrids.executions.history.status.recorded", "Recorded"]
|
|
478
|
-
};
|
|
479
|
-
function isHttpsUrl(value) {
|
|
480
|
-
if (!value) return false;
|
|
481
|
-
try {
|
|
482
|
-
return new URL(value).protocol === "https:";
|
|
483
|
-
} catch {
|
|
484
|
-
return false;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
function formatTime(value) {
|
|
488
|
-
const date = new Date(value);
|
|
489
|
-
if (Number.isNaN(date.getTime())) return value;
|
|
490
|
-
return date.toLocaleTimeString("en-US", {
|
|
491
|
-
hour: "2-digit",
|
|
492
|
-
minute: "2-digit",
|
|
493
|
-
second: "2-digit"
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
function RunHistoryView({
|
|
497
|
-
events,
|
|
498
|
-
loading,
|
|
499
|
-
error,
|
|
500
|
-
active,
|
|
501
|
-
streaming,
|
|
502
|
-
truncated = false,
|
|
503
|
-
historyLength,
|
|
504
|
-
onRefresh,
|
|
505
|
-
onViewChildRun,
|
|
506
|
-
defaultShowTasks = false,
|
|
507
|
-
compact = false,
|
|
508
|
-
showBenign = false,
|
|
509
|
-
"data-testid": testId = "run-history"
|
|
510
|
-
}) {
|
|
511
|
-
const tr = useTr();
|
|
512
|
-
const [showTasks, setShowTasks] = useState(defaultShowTasks);
|
|
513
|
-
const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
|
|
514
|
-
const groups = useMemo(() => groupRunHistory(events), [events]);
|
|
515
|
-
const visible = useMemo(
|
|
516
|
-
() => showTasks ? groups : groups.filter((g) => g.kind !== "task"),
|
|
517
|
-
[groups, showTasks]
|
|
518
|
-
);
|
|
519
|
-
const toggle = (key) => setExpanded((current) => {
|
|
520
|
-
const next = new Set(current);
|
|
521
|
-
if (next.has(key)) next.delete(key);
|
|
522
|
-
else next.add(key);
|
|
523
|
-
return next;
|
|
524
|
-
});
|
|
525
|
-
return /* @__PURE__ */ jsxs("div", { "data-testid": testId, className: "flex flex-col gap-3", children: [
|
|
526
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3 text-sm text-text-secondary", children: [
|
|
527
|
-
/* @__PURE__ */ jsx("span", { "data-testid": `${testId}-count`, children: tr("fluidgrids.executions.history.eventCount", "{{count}} events", {
|
|
528
|
-
count: historyLength ?? events.length
|
|
529
|
-
}) }),
|
|
530
|
-
active && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1", children: [
|
|
531
|
-
/* @__PURE__ */ jsx(
|
|
532
|
-
Icon,
|
|
533
|
-
{
|
|
534
|
-
icon: streaming ? "mdi:access-point" : "mdi:refresh",
|
|
535
|
-
width: 14
|
|
536
|
-
}
|
|
537
|
-
),
|
|
538
|
-
streaming ? tr("fluidgrids.executions.history.live", "Live") : tr("fluidgrids.executions.history.polling", "Refreshing")
|
|
539
|
-
] }),
|
|
540
|
-
/* @__PURE__ */ jsxs("label", { className: "ml-auto flex items-center gap-2", children: [
|
|
541
|
-
/* @__PURE__ */ jsx(
|
|
542
|
-
"input",
|
|
543
|
-
{
|
|
544
|
-
type: "checkbox",
|
|
545
|
-
checked: showTasks,
|
|
546
|
-
onChange: (e) => setShowTasks(e.target.checked),
|
|
547
|
-
className: "h-4 w-4 rounded",
|
|
548
|
-
"data-testid": `${testId}-show-tasks`
|
|
549
|
-
}
|
|
550
|
-
),
|
|
551
|
-
tr("fluidgrids.executions.history.showTasks", "Show workflow tasks")
|
|
552
|
-
] }),
|
|
553
|
-
onRefresh && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onRefresh, children: [
|
|
554
|
-
/* @__PURE__ */ jsx(Icon, { icon: "mdi:refresh", className: "mr-1", width: 16 }),
|
|
555
|
-
tr("fluidgrids.executions.history.refresh", "Refresh")
|
|
556
|
-
] })
|
|
557
|
-
] }),
|
|
558
|
-
error && /* @__PURE__ */ jsxs(
|
|
559
|
-
"div",
|
|
560
|
-
{
|
|
561
|
-
role: "alert",
|
|
562
|
-
className: "rounded-md border border-[var(--color-status-error-border)] bg-[var(--color-status-error-bg-subtle)] p-3 text-sm text-[var(--color-status-error-text)]",
|
|
563
|
-
children: [
|
|
564
|
-
tr(
|
|
565
|
-
"fluidgrids.executions.history.loadError",
|
|
566
|
-
"Could not load the run history:"
|
|
567
|
-
),
|
|
568
|
-
" ",
|
|
569
|
-
error.message
|
|
570
|
-
]
|
|
571
|
-
}
|
|
572
|
-
),
|
|
573
|
-
truncated && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-[var(--color-status-warning-border)] bg-[var(--color-status-warning-bg-subtle)] p-3 text-sm text-[var(--color-status-warning-text)]", children: tr(
|
|
574
|
-
"fluidgrids.executions.history.truncated",
|
|
575
|
-
"Only the first events are shown. Use fluidgrids history <runId> for the full history."
|
|
576
|
-
) }),
|
|
577
|
-
loading && events.length === 0 ? /* @__PURE__ */ jsx("div", { className: "space-y-2", "aria-busy": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx(
|
|
578
|
-
"div",
|
|
579
|
-
{
|
|
580
|
-
className: "h-10 animate-pulse rounded bg-background-tertiary"
|
|
581
|
-
},
|
|
582
|
-
i
|
|
583
|
-
)) }) : visible.length === 0 && !error ? /* @__PURE__ */ jsx("p", { className: "py-6 text-center text-sm text-text-secondary", children: tr("fluidgrids.executions.history.empty", "No events recorded yet.") }) : /* @__PURE__ */ jsx("ol", { className: "flex flex-col divide-y divide-border-secondary rounded-md border border-border-secondary", children: visible.map((group) => /* @__PURE__ */ jsx(
|
|
584
|
-
HistoryRow,
|
|
585
|
-
{
|
|
586
|
-
group,
|
|
587
|
-
expanded: expanded.has(group.key),
|
|
588
|
-
onToggle: () => toggle(group.key),
|
|
589
|
-
onViewChildRun,
|
|
590
|
-
compact,
|
|
591
|
-
benign: showBenign && group.failure?.benign === true,
|
|
592
|
-
statusLabel: tr(...STATUS_LABEL[group.status])
|
|
593
|
-
},
|
|
594
|
-
group.key
|
|
595
|
-
)) })
|
|
596
|
-
] });
|
|
597
|
-
}
|
|
598
|
-
function HistoryRow({
|
|
599
|
-
group,
|
|
600
|
-
expanded,
|
|
601
|
-
onToggle,
|
|
602
|
-
onViewChildRun,
|
|
603
|
-
compact,
|
|
604
|
-
benign,
|
|
605
|
-
statusLabel
|
|
606
|
-
}) {
|
|
607
|
-
const tr = useTr();
|
|
608
|
-
return /* @__PURE__ */ jsxs(
|
|
609
|
-
"li",
|
|
610
|
-
{
|
|
611
|
-
"data-testid": `history-group-${group.key}`,
|
|
612
|
-
"data-status": group.status,
|
|
613
|
-
"data-benign": benign ? "true" : void 0,
|
|
614
|
-
children: [
|
|
615
|
-
/* @__PURE__ */ jsxs(
|
|
616
|
-
"button",
|
|
617
|
-
{
|
|
618
|
-
type: "button",
|
|
619
|
-
onClick: onToggle,
|
|
620
|
-
"aria-expanded": expanded,
|
|
621
|
-
className: cn(
|
|
622
|
-
"flex w-full items-center gap-3 text-left hover:bg-background-tertiary",
|
|
623
|
-
compact ? "px-2 py-1.5" : "px-3 py-2"
|
|
624
|
-
),
|
|
625
|
-
children: [
|
|
626
|
-
/* @__PURE__ */ jsx(
|
|
627
|
-
Icon,
|
|
628
|
-
{
|
|
629
|
-
icon: expanded ? "mdi:chevron-down" : "mdi:chevron-right",
|
|
630
|
-
width: 16,
|
|
631
|
-
className: "shrink-0 text-text-secondary"
|
|
632
|
-
}
|
|
633
|
-
),
|
|
634
|
-
/* @__PURE__ */ jsx(
|
|
635
|
-
Icon,
|
|
636
|
-
{
|
|
637
|
-
icon: KIND_ICONS[group.kind],
|
|
638
|
-
width: 16,
|
|
639
|
-
className: "shrink-0 text-text-secondary"
|
|
640
|
-
}
|
|
641
|
-
),
|
|
642
|
-
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
643
|
-
/* @__PURE__ */ jsx("span", { className: "block truncate text-sm font-medium text-text-primary", children: group.label }),
|
|
644
|
-
group.detail && /* @__PURE__ */ jsx("span", { className: "block truncate font-mono text-xs text-text-secondary", children: group.detail })
|
|
645
|
-
] }),
|
|
646
|
-
group.attempt !== void 0 && group.attempt > 1 && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-xs text-text-secondary", children: tr(
|
|
647
|
-
"fluidgrids.executions.history.attempt",
|
|
648
|
-
"attempt {{attempt}}",
|
|
649
|
-
{
|
|
650
|
-
attempt: group.attempt
|
|
651
|
-
}
|
|
652
|
-
) }),
|
|
653
|
-
/* @__PURE__ */ jsx(
|
|
654
|
-
"span",
|
|
655
|
-
{
|
|
656
|
-
className: cn(
|
|
657
|
-
"shrink-0 rounded border px-1.5 py-0.5 text-xs",
|
|
658
|
-
benign ? BENIGN_CLASS : STATUS_CLASS[group.status]
|
|
659
|
-
),
|
|
660
|
-
children: benign ? tr("fluidgrids.executions.history.status.benign", "Expected") : statusLabel
|
|
661
|
-
}
|
|
662
|
-
),
|
|
663
|
-
!compact && /* @__PURE__ */ jsx("span", { className: "w-20 shrink-0 text-right font-mono text-xs text-text-secondary", children: formatTime(group.startedAt) })
|
|
664
|
-
]
|
|
665
|
-
}
|
|
666
|
-
),
|
|
667
|
-
group.failure && /* @__PURE__ */ jsxs(
|
|
668
|
-
"p",
|
|
669
|
-
{
|
|
670
|
-
className: cn(
|
|
671
|
-
"px-10 pb-2 text-xs",
|
|
672
|
-
benign ? "text-text-secondary" : "text-[var(--color-status-error-text)]"
|
|
673
|
-
),
|
|
674
|
-
children: [
|
|
675
|
-
group.failure.errorType ? `${group.failure.errorType}: ` : "",
|
|
676
|
-
group.failure.message
|
|
677
|
-
]
|
|
678
|
-
}
|
|
679
|
-
),
|
|
680
|
-
expanded && /* @__PURE__ */ jsxs("div", { className: "space-y-2 bg-background-primary px-10 py-2", children: [
|
|
681
|
-
group.childRunId && onViewChildRun && /* @__PURE__ */ jsxs(
|
|
682
|
-
Button,
|
|
683
|
-
{
|
|
684
|
-
variant: "outline",
|
|
685
|
-
size: "sm",
|
|
686
|
-
onClick: () => onViewChildRun(group.childRunId),
|
|
687
|
-
children: [
|
|
688
|
-
/* @__PURE__ */ jsx(Icon, { icon: "mdi:open-in-new", className: "mr-1", width: 14 }),
|
|
689
|
-
tr("fluidgrids.executions.history.openChild", "Open child run")
|
|
690
|
-
]
|
|
691
|
-
}
|
|
692
|
-
),
|
|
693
|
-
group.events.map((event) => /* @__PURE__ */ jsxs("div", { className: "text-xs", children: [
|
|
694
|
-
/* @__PURE__ */ jsxs("div", { className: "flex gap-3 font-mono text-text-secondary", children: [
|
|
695
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
696
|
-
"#",
|
|
697
|
-
event.seq
|
|
698
|
-
] }),
|
|
699
|
-
/* @__PURE__ */ jsx("span", { className: "text-text-primary", children: event.type }),
|
|
700
|
-
/* @__PURE__ */ jsx("span", { className: "ml-auto", children: formatTime(event.createdAt) })
|
|
701
|
-
] }),
|
|
702
|
-
isHttpsUrl(event.payloadUrl) ? /* @__PURE__ */ jsx(
|
|
703
|
-
"a",
|
|
704
|
-
{
|
|
705
|
-
href: event.payloadUrl ?? void 0,
|
|
706
|
-
target: "_blank",
|
|
707
|
-
rel: "noreferrer noopener",
|
|
708
|
-
className: "text-action-primary-text hover:underline",
|
|
709
|
-
children: tr(
|
|
710
|
-
"fluidgrids.executions.history.offloadedPayload",
|
|
711
|
-
"Download payload"
|
|
712
|
-
)
|
|
713
|
-
}
|
|
714
|
-
) : event.payload && Object.keys(event.payload).length > 0 ? /* @__PURE__ */ jsx("pre", { className: "mt-1 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded bg-background-secondary p-2 font-mono text-text-primary", children: JSON.stringify(event.payload, null, 2) }) : null
|
|
715
|
-
] }, event.seq))
|
|
716
|
-
] })
|
|
717
|
-
]
|
|
718
|
-
}
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
async function fetchDurableRunFields(runId) {
|
|
722
|
-
const result = await getFluidGridsClient().query({
|
|
723
|
-
query: DurableRunFieldsDocument,
|
|
724
|
-
variables: { id: runId },
|
|
725
|
-
fetchPolicy: "network-only"
|
|
726
|
-
});
|
|
727
|
-
throwIfResultError(result);
|
|
728
|
-
return result.data?.workflowRun ?? null;
|
|
729
|
-
}
|
|
730
|
-
function useDurableRunFields(runId, enabled) {
|
|
731
|
-
const [fields, setFields] = useState(null);
|
|
732
|
-
const request = useRef(0);
|
|
733
|
-
const isGraphRun = useRef(false);
|
|
734
|
-
const read = useCallback(async () => {
|
|
735
|
-
if (!runId || !enabled) return;
|
|
736
|
-
const id = ++request.current;
|
|
737
|
-
try {
|
|
738
|
-
const next = await fetchDurableRunFields(runId);
|
|
739
|
-
if (id !== request.current) return;
|
|
740
|
-
isGraphRun.current = next?.executionKind === "GRAPH" /* Graph */;
|
|
741
|
-
setFields(next);
|
|
742
|
-
} catch {
|
|
743
|
-
if (id !== request.current) return;
|
|
744
|
-
}
|
|
745
|
-
}, [runId, enabled]);
|
|
746
|
-
useEffect(() => {
|
|
747
|
-
request.current += 1;
|
|
748
|
-
isGraphRun.current = false;
|
|
749
|
-
setFields(null);
|
|
750
|
-
void read();
|
|
751
|
-
}, [read]);
|
|
752
|
-
const refetch = useCallback(async () => {
|
|
753
|
-
if (isGraphRun.current) return;
|
|
754
|
-
await read();
|
|
755
|
-
}, [read]);
|
|
756
|
-
return { fields: enabled ? fields : null, refetch };
|
|
757
|
-
}
|
|
758
|
-
var DETERMINISM_POLL_INTERVAL_MS = 1e4;
|
|
759
|
-
async function fetchDeterminismStatus(runId) {
|
|
760
|
-
const result = await getFluidGridsClient().query({
|
|
761
|
-
query: WorkflowRunDeterminismStatusDocument,
|
|
762
|
-
variables: { runId },
|
|
763
|
-
fetchPolicy: "network-only"
|
|
764
|
-
});
|
|
765
|
-
throwIfResultError(result);
|
|
766
|
-
return result.data?.workflowRunDeterminismStatus ?? null;
|
|
767
|
-
}
|
|
768
|
-
async function fetchRunStackTrace(runId) {
|
|
769
|
-
const result = await getFluidGridsClient().query({
|
|
770
|
-
query: WorkflowRunStackTraceDocument,
|
|
771
|
-
variables: { runId, enhanced: false },
|
|
772
|
-
fetchPolicy: "network-only"
|
|
773
|
-
});
|
|
774
|
-
throwIfResultError(result);
|
|
775
|
-
return result.data?.workflowRunStackTrace.trace ?? null;
|
|
776
|
-
}
|
|
777
|
-
function newSignalIdempotencyKey() {
|
|
778
|
-
return crypto.randomUUID();
|
|
779
|
-
}
|
|
780
|
-
async function sendRunSignal(input) {
|
|
781
|
-
const result = await getFluidGridsClient().mutate({
|
|
782
|
-
mutation: SignalWorkflowRunDocument,
|
|
783
|
-
variables: {
|
|
784
|
-
runId: input.runId,
|
|
785
|
-
name: input.name,
|
|
786
|
-
// The JSON scalar carries any value; the generated type is the object
|
|
787
|
-
// form only, and the service validates the payload against the schema.
|
|
788
|
-
payload: input.payload,
|
|
789
|
-
idempotencyKey: input.idempotencyKey
|
|
790
|
-
}
|
|
791
|
-
});
|
|
792
|
-
throwIfResultError(result);
|
|
793
|
-
const data = result.data?.signalWorkflowRun;
|
|
794
|
-
if (!data) throw new Error("The signal returned no result");
|
|
795
|
-
return data;
|
|
796
|
-
}
|
|
797
|
-
async function terminateDurableRun(runId, reason) {
|
|
798
|
-
const result = await getFluidGridsClient().mutate({
|
|
799
|
-
mutation: TerminateWorkflowRunDocument,
|
|
800
|
-
variables: { runId, reason }
|
|
801
|
-
});
|
|
802
|
-
throwIfResultError(result);
|
|
803
|
-
}
|
|
804
|
-
function useDeterminismStatus(runId, { enabled, live }) {
|
|
805
|
-
const [status, setStatus] = useState(null);
|
|
806
|
-
const [error, setError] = useState(null);
|
|
807
|
-
const request = useRef(0);
|
|
808
|
-
const refetch = useCallback(async () => {
|
|
809
|
-
if (!runId || !enabled) return;
|
|
810
|
-
const id = ++request.current;
|
|
811
|
-
try {
|
|
812
|
-
const next = await fetchDeterminismStatus(runId);
|
|
813
|
-
if (id !== request.current) return;
|
|
814
|
-
setStatus(next);
|
|
815
|
-
setError(null);
|
|
816
|
-
} catch (err) {
|
|
817
|
-
if (id !== request.current) return;
|
|
818
|
-
setError(describeOperationError(err));
|
|
819
|
-
}
|
|
820
|
-
}, [runId, enabled]);
|
|
821
|
-
useEffect(() => {
|
|
822
|
-
request.current += 1;
|
|
823
|
-
setStatus(null);
|
|
824
|
-
setError(null);
|
|
825
|
-
void refetch();
|
|
826
|
-
}, [refetch]);
|
|
827
|
-
useEffect(() => {
|
|
828
|
-
if (!live || !enabled || !runId) return;
|
|
829
|
-
const timer = setInterval(() => {
|
|
830
|
-
void refetch();
|
|
831
|
-
}, DETERMINISM_POLL_INTERVAL_MS);
|
|
832
|
-
return () => clearInterval(timer);
|
|
833
|
-
}, [live, enabled, runId, refetch]);
|
|
834
|
-
return { status, error, refetch };
|
|
835
|
-
}
|
|
836
|
-
function useRunStackTrace(runId, enabled) {
|
|
837
|
-
const [trace, setTrace] = useState(null);
|
|
838
|
-
const [loading, setLoading] = useState(false);
|
|
839
|
-
const [error, setError] = useState(null);
|
|
840
|
-
const request = useRef(0);
|
|
841
|
-
const refetch = useCallback(async () => {
|
|
842
|
-
if (!runId || !enabled) return;
|
|
843
|
-
const id = ++request.current;
|
|
844
|
-
setLoading(true);
|
|
845
|
-
try {
|
|
846
|
-
const next = await fetchRunStackTrace(runId);
|
|
847
|
-
if (id !== request.current) return;
|
|
848
|
-
setTrace(next);
|
|
849
|
-
setError(null);
|
|
850
|
-
} catch (err) {
|
|
851
|
-
if (id !== request.current) return;
|
|
852
|
-
setError(describeOperationError(err));
|
|
853
|
-
} finally {
|
|
854
|
-
if (id === request.current) setLoading(false);
|
|
855
|
-
}
|
|
856
|
-
}, [runId, enabled]);
|
|
857
|
-
useEffect(() => {
|
|
858
|
-
request.current += 1;
|
|
859
|
-
setTrace(null);
|
|
860
|
-
setError(null);
|
|
861
|
-
setLoading(false);
|
|
862
|
-
void refetch();
|
|
863
|
-
}, [refetch]);
|
|
864
|
-
return { trace, loading, error, refetch };
|
|
865
|
-
}
|
|
866
|
-
function useOpenSnapshot(open, value) {
|
|
867
|
-
const [snapshot, setSnapshot] = useState(
|
|
868
|
-
() => open ? { value } : null
|
|
869
|
-
);
|
|
870
|
-
if (open && snapshot === null) {
|
|
871
|
-
setSnapshot({ value });
|
|
872
|
-
return value;
|
|
873
|
-
}
|
|
874
|
-
if (!open && snapshot !== null) {
|
|
875
|
-
setSnapshot(null);
|
|
876
|
-
}
|
|
877
|
-
return snapshot === null ? value : snapshot.value;
|
|
878
|
-
}
|
|
879
|
-
var CUSTOM = "__custom__";
|
|
880
|
-
function CustomSignalNameInput({
|
|
881
|
-
value,
|
|
882
|
-
onChange
|
|
883
|
-
}) {
|
|
884
|
-
const tr = useTr();
|
|
885
|
-
return /* @__PURE__ */ jsx(
|
|
886
|
-
Input,
|
|
887
|
-
{
|
|
888
|
-
value,
|
|
889
|
-
onChange: (e) => onChange(e.target.value),
|
|
890
|
-
placeholder: tr(
|
|
891
|
-
"fluidgrids.executions.signal.namePlaceholder",
|
|
892
|
-
"signal-name"
|
|
893
|
-
),
|
|
894
|
-
"aria-label": tr(
|
|
895
|
-
"fluidgrids.executions.signal.customNameLabel",
|
|
896
|
-
"Signal name"
|
|
897
|
-
),
|
|
898
|
-
"data-testid": "signal-custom-name"
|
|
899
|
-
}
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
function PayloadModeToggle({
|
|
903
|
-
mode,
|
|
904
|
-
onChange
|
|
905
|
-
}) {
|
|
906
|
-
const tr = useTr();
|
|
907
|
-
return /* @__PURE__ */ jsxs(
|
|
908
|
-
"div",
|
|
909
|
-
{
|
|
910
|
-
className: "flex gap-2 text-xs",
|
|
911
|
-
role: "group",
|
|
912
|
-
"aria-label": tr(
|
|
913
|
-
"fluidgrids.executions.signal.payloadMode",
|
|
914
|
-
"Payload editor"
|
|
915
|
-
),
|
|
916
|
-
children: [
|
|
917
|
-
/* @__PURE__ */ jsx(
|
|
918
|
-
Button,
|
|
919
|
-
{
|
|
920
|
-
variant: mode === "form" ? "default" : "outline",
|
|
921
|
-
size: "sm",
|
|
922
|
-
"aria-pressed": mode === "form",
|
|
923
|
-
onClick: () => onChange("form"),
|
|
924
|
-
children: tr("fluidgrids.executions.signal.formMode", "Form")
|
|
925
|
-
}
|
|
926
|
-
),
|
|
927
|
-
/* @__PURE__ */ jsx(
|
|
928
|
-
Button,
|
|
929
|
-
{
|
|
930
|
-
variant: mode === "json" ? "default" : "outline",
|
|
931
|
-
size: "sm",
|
|
932
|
-
"aria-pressed": mode === "json",
|
|
933
|
-
onClick: () => onChange("json"),
|
|
934
|
-
children: tr("fluidgrids.executions.signal.jsonMode", "JSON")
|
|
935
|
-
}
|
|
936
|
-
)
|
|
937
|
-
]
|
|
938
|
-
}
|
|
939
|
-
);
|
|
940
|
-
}
|
|
941
|
-
function SignalRunDialog({
|
|
942
|
-
runId,
|
|
943
|
-
targets: currentTargets,
|
|
944
|
-
open,
|
|
945
|
-
onOpenChange,
|
|
946
|
-
onSent,
|
|
947
|
-
initialName: currentInitialName
|
|
948
|
-
}) {
|
|
949
|
-
const tr = useTr();
|
|
950
|
-
const { targets, initialName } = useOpenSnapshot(open, {
|
|
951
|
-
targets: currentTargets,
|
|
952
|
-
initialName: currentInitialName
|
|
953
|
-
});
|
|
954
|
-
const firstName = initialName ?? targets.targets[0]?.name ?? CUSTOM;
|
|
955
|
-
const [selected, setSelected] = useState(firstName);
|
|
956
|
-
const [customName, setCustomName] = useState("");
|
|
957
|
-
const [mode, setMode] = useState("form");
|
|
958
|
-
const [rawPayload, setRawPayload] = useState("");
|
|
959
|
-
const [error, setError] = useState(null);
|
|
960
|
-
const [sending, setSending] = useState(false);
|
|
961
|
-
const [result, setResult] = useState(null);
|
|
962
|
-
const idempotencyKey = useRef(newSignalIdempotencyKey());
|
|
963
|
-
const cardRef = useRef(null);
|
|
964
|
-
useEffect(() => {
|
|
965
|
-
if (!open) return;
|
|
966
|
-
idempotencyKey.current = newSignalIdempotencyKey();
|
|
967
|
-
setSelected(firstName);
|
|
968
|
-
setCustomName("");
|
|
969
|
-
setRawPayload("");
|
|
970
|
-
setError(null);
|
|
971
|
-
setResult(null);
|
|
972
|
-
setSending(false);
|
|
973
|
-
}, [open, firstName]);
|
|
974
|
-
const target = targets.targets.find((t) => t.name === selected);
|
|
975
|
-
const name = selected === CUSTOM ? customName.trim() : selected;
|
|
976
|
-
const card = useMemo(
|
|
977
|
-
() => signalPayloadCard(target?.payloadSchema),
|
|
978
|
-
[target?.payloadSchema]
|
|
979
|
-
);
|
|
980
|
-
const effectiveMode = card ? mode : "json";
|
|
981
|
-
useEffect(() => {
|
|
982
|
-
idempotencyKey.current = newSignalIdempotencyKey();
|
|
983
|
-
setResult(null);
|
|
984
|
-
setError(null);
|
|
985
|
-
}, [selected]);
|
|
986
|
-
const send = async () => {
|
|
987
|
-
setError(null);
|
|
988
|
-
if (!name) {
|
|
989
|
-
setError(
|
|
990
|
-
tr("fluidgrids.executions.signal.nameRequired", "Enter a signal name.")
|
|
991
|
-
);
|
|
992
|
-
return;
|
|
993
|
-
}
|
|
994
|
-
let payload;
|
|
995
|
-
if (effectiveMode === "form" && card) {
|
|
996
|
-
if (cardRef.current && !cardRef.current.validateInputs()) {
|
|
997
|
-
setError(
|
|
998
|
-
tr(
|
|
999
|
-
"fluidgrids.executions.signal.formInvalid",
|
|
1000
|
-
"Fill in the required fields."
|
|
1001
|
-
)
|
|
1002
|
-
);
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
const parsed = payloadFromCardValues(
|
|
1006
|
-
target?.payloadSchema,
|
|
1007
|
-
cardRef.current?.collectValues() ?? {}
|
|
1008
|
-
);
|
|
1009
|
-
if ("error" in parsed) {
|
|
1010
|
-
setError(parsed.error);
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
payload = parsed.payload;
|
|
1014
|
-
} else {
|
|
1015
|
-
const parsed = parseRawSignalPayload(rawPayload);
|
|
1016
|
-
if ("error" in parsed) {
|
|
1017
|
-
setError(
|
|
1018
|
-
tr(
|
|
1019
|
-
"fluidgrids.executions.signal.jsonInvalid",
|
|
1020
|
-
"The payload is not valid JSON: {{message}}",
|
|
1021
|
-
{ message: parsed.error }
|
|
1022
|
-
)
|
|
1023
|
-
);
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
payload = parsed.payload;
|
|
1027
|
-
}
|
|
1028
|
-
setSending(true);
|
|
1029
|
-
try {
|
|
1030
|
-
const sent = await sendRunSignal({
|
|
1031
|
-
runId,
|
|
1032
|
-
name,
|
|
1033
|
-
payload,
|
|
1034
|
-
idempotencyKey: idempotencyKey.current
|
|
1035
|
-
});
|
|
1036
|
-
setResult(sent);
|
|
1037
|
-
onSent?.(sent);
|
|
1038
|
-
} catch (err) {
|
|
1039
|
-
setError(describeOperationError(err));
|
|
1040
|
-
} finally {
|
|
1041
|
-
setSending(false);
|
|
1042
|
-
}
|
|
1043
|
-
};
|
|
1044
|
-
return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs(DialogContent, { "data-testid": "signal-run-dialog", className: "max-w-lg", children: [
|
|
1045
|
-
/* @__PURE__ */ jsxs(DialogHeader, { children: [
|
|
1046
|
-
/* @__PURE__ */ jsx(DialogTitle, { children: tr("fluidgrids.executions.signal.title", "Send a signal") }),
|
|
1047
|
-
/* @__PURE__ */ jsx(DialogDescription, { children: tr(
|
|
1048
|
-
"fluidgrids.executions.signal.description",
|
|
1049
|
-
"The run receives the signal by name. A payload that does not match the signal's schema is refused."
|
|
1050
|
-
) })
|
|
1051
|
-
] }),
|
|
1052
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
1053
|
-
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
1054
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: "signal-name", children: tr("fluidgrids.executions.signal.nameLabel", "Signal") }),
|
|
1055
|
-
/* @__PURE__ */ jsxs(
|
|
1056
|
-
"select",
|
|
1057
|
-
{
|
|
1058
|
-
id: "signal-name",
|
|
1059
|
-
value: selected,
|
|
1060
|
-
onChange: (e) => setSelected(e.target.value),
|
|
1061
|
-
className: "w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground",
|
|
1062
|
-
"data-testid": "signal-name-select",
|
|
1063
|
-
children: [
|
|
1064
|
-
targets.targets.map((t) => /* @__PURE__ */ jsx("option", { value: t.name, children: t.name }, `${t.nodeId ?? ""}:${t.name}`)),
|
|
1065
|
-
targets.openNames && /* @__PURE__ */ jsx("option", { value: CUSTOM, children: tr(
|
|
1066
|
-
"fluidgrids.executions.signal.otherName",
|
|
1067
|
-
"Another name\u2026"
|
|
1068
|
-
) })
|
|
1069
|
-
]
|
|
1070
|
-
}
|
|
1071
|
-
),
|
|
1072
|
-
selected === CUSTOM && /* @__PURE__ */ jsx(
|
|
1073
|
-
CustomSignalNameInput,
|
|
1074
|
-
{
|
|
1075
|
-
value: customName,
|
|
1076
|
-
onChange: setCustomName
|
|
1077
|
-
}
|
|
1078
|
-
)
|
|
1079
|
-
] }),
|
|
1080
|
-
card && /* @__PURE__ */ jsx(PayloadModeToggle, { mode: effectiveMode, onChange: setMode }),
|
|
1081
|
-
effectiveMode === "form" && card ? /* @__PURE__ */ jsx(components_default, { ref: cardRef, cardData: card }) : /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
1082
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: "signal-payload", children: tr(
|
|
1083
|
-
"fluidgrids.executions.signal.payloadLabel",
|
|
1084
|
-
"Payload (JSON)"
|
|
1085
|
-
) }),
|
|
1086
|
-
/* @__PURE__ */ jsx(
|
|
1087
|
-
Textarea,
|
|
1088
|
-
{
|
|
1089
|
-
id: "signal-payload",
|
|
1090
|
-
value: rawPayload,
|
|
1091
|
-
onChange: (e) => setRawPayload(e.target.value),
|
|
1092
|
-
rows: 6,
|
|
1093
|
-
className: "font-mono text-sm",
|
|
1094
|
-
placeholder: '{ "approved": true }',
|
|
1095
|
-
"data-testid": "signal-payload-json"
|
|
1096
|
-
}
|
|
1097
|
-
)
|
|
1098
|
-
] }),
|
|
1099
|
-
error && /* @__PURE__ */ jsx(
|
|
1100
|
-
"p",
|
|
1101
|
-
{
|
|
1102
|
-
role: "alert",
|
|
1103
|
-
className: "text-sm text-[var(--color-status-error-text)]",
|
|
1104
|
-
children: error
|
|
1105
|
-
}
|
|
1106
|
-
),
|
|
1107
|
-
result && /* @__PURE__ */ jsxs(
|
|
1108
|
-
"p",
|
|
1109
|
-
{
|
|
1110
|
-
"data-testid": "signal-result",
|
|
1111
|
-
className: "flex items-center gap-2 text-sm text-[var(--color-status-success-text)]",
|
|
1112
|
-
children: [
|
|
1113
|
-
/* @__PURE__ */ jsx(Icon, { icon: "mdi:check-circle-outline", width: 16 }),
|
|
1114
|
-
result.duplicate ? tr(
|
|
1115
|
-
"fluidgrids.executions.signal.duplicate",
|
|
1116
|
-
"Already sent; the run recorded it once."
|
|
1117
|
-
) : result.buffered ? tr(
|
|
1118
|
-
"fluidgrids.executions.signal.buffered",
|
|
1119
|
-
"Sent. The run will receive it when it reaches its wait."
|
|
1120
|
-
) : result.forwardedToRunId ? tr(
|
|
1121
|
-
"fluidgrids.executions.signal.forwarded",
|
|
1122
|
-
"Sent to the child run that is waiting for it."
|
|
1123
|
-
) : tr(
|
|
1124
|
-
"fluidgrids.executions.signal.delivered",
|
|
1125
|
-
"Sent and delivered."
|
|
1126
|
-
)
|
|
1127
|
-
]
|
|
1128
|
-
}
|
|
1129
|
-
)
|
|
1130
|
-
] }),
|
|
1131
|
-
/* @__PURE__ */ jsxs(DialogFooter, { children: [
|
|
1132
|
-
/* @__PURE__ */ jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: result ? tr("fluidgrids.executions.signal.close", "Close") : tr("fluidgrids.executions.signal.cancel", "Cancel") }),
|
|
1133
|
-
!result && /* @__PURE__ */ jsx(
|
|
1134
|
-
Button,
|
|
1135
|
-
{
|
|
1136
|
-
onClick: () => void send(),
|
|
1137
|
-
disabled: sending,
|
|
1138
|
-
"data-testid": "signal-send",
|
|
1139
|
-
children: sending ? tr("fluidgrids.executions.signal.sending", "Sending...") : tr("fluidgrids.executions.signal.send", "Send")
|
|
1140
|
-
}
|
|
1141
|
-
)
|
|
1142
|
-
] })
|
|
1143
|
-
] }) });
|
|
1144
|
-
}
|
|
1145
|
-
var RUN_HISTORY_POLL_INTERVAL_MS = 2e3;
|
|
1146
|
-
var RUN_HISTORY_SAFETY_READ_INITIAL_MS = 15e3;
|
|
1147
|
-
var RUN_HISTORY_SAFETY_READ_MAX_MS = 12e4;
|
|
1148
|
-
var RUN_HISTORY_PAGE_SIZE = 500;
|
|
1149
|
-
var RUN_HISTORY_MAX_PAGES = 20;
|
|
1150
|
-
function toEvents(events) {
|
|
1151
|
-
return events.map((event) => ({
|
|
1152
|
-
runId: event.runId,
|
|
1153
|
-
seq: event.seq,
|
|
1154
|
-
type: event.type,
|
|
1155
|
-
taskId: event.taskId ?? null,
|
|
1156
|
-
payload: event.payload ?? null,
|
|
1157
|
-
payloadUrl: event.payloadUrl ?? null,
|
|
1158
|
-
createdAt: event.createdAt
|
|
1159
|
-
}));
|
|
1160
|
-
}
|
|
1161
|
-
function useRunHistory(runId, options = {}) {
|
|
1162
|
-
const { live = false, enabled = true } = options;
|
|
1163
|
-
const [events, setEvents] = useState([]);
|
|
1164
|
-
const [loading, setLoading] = useState(enabled && !!runId);
|
|
1165
|
-
const [error, setError] = useState(null);
|
|
1166
|
-
const [truncated, setTruncated] = useState(false);
|
|
1167
|
-
const [historyLength, setHistoryLength] = useState(null);
|
|
1168
|
-
const [streaming, setStreaming] = useState(false);
|
|
1169
|
-
const cursor = useRef(0);
|
|
1170
|
-
const generation = useRef(0);
|
|
1171
|
-
const append = useCallback((incoming) => {
|
|
1172
|
-
if (!incoming.length) return;
|
|
1173
|
-
setEvents((current) => {
|
|
1174
|
-
const next = mergeHistoryEvents(current, incoming);
|
|
1175
|
-
cursor.current = Math.max(cursor.current, lastSeqOf(next));
|
|
1176
|
-
return next;
|
|
1177
|
-
});
|
|
1178
|
-
}, []);
|
|
1179
|
-
const readForward = useCallback(
|
|
1180
|
-
async (maxPages) => {
|
|
1181
|
-
const client = getFluidGridsClientSafe();
|
|
1182
|
-
if (!client || !runId) return;
|
|
1183
|
-
const started = generation.current;
|
|
1184
|
-
for (let page = 0; page < maxPages; page += 1) {
|
|
1185
|
-
const result = await client.query({
|
|
1186
|
-
query: WorkflowRunHistoryDocument,
|
|
1187
|
-
variables: {
|
|
1188
|
-
runId,
|
|
1189
|
-
after: cursor.current,
|
|
1190
|
-
limit: RUN_HISTORY_PAGE_SIZE
|
|
1191
|
-
},
|
|
1192
|
-
fetchPolicy: "network-only"
|
|
1193
|
-
});
|
|
1194
|
-
throwIfResultError(result);
|
|
1195
|
-
if (generation.current !== started) return;
|
|
1196
|
-
const pageData = result.data?.workflowRunHistory;
|
|
1197
|
-
if (!pageData) return;
|
|
1198
|
-
const fresh = toEvents(pageData.events);
|
|
1199
|
-
append(fresh);
|
|
1200
|
-
cursor.current = Math.max(
|
|
1201
|
-
cursor.current,
|
|
1202
|
-
lastSeqOf(fresh),
|
|
1203
|
-
pageData.nextAfter ?? 0
|
|
1204
|
-
);
|
|
1205
|
-
setHistoryLength(pageData.historyLength);
|
|
1206
|
-
if (!pageData.hasMore || fresh.length === 0) {
|
|
1207
|
-
setTruncated(false);
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
if (page === maxPages - 1) setTruncated(true);
|
|
1211
|
-
}
|
|
1212
|
-
},
|
|
1213
|
-
[runId, append]
|
|
1214
|
-
);
|
|
1215
|
-
const refetch = useCallback(async () => {
|
|
1216
|
-
generation.current += 1;
|
|
1217
|
-
cursor.current = 0;
|
|
1218
|
-
setEvents([]);
|
|
1219
|
-
setTruncated(false);
|
|
1220
|
-
setError(null);
|
|
1221
|
-
setLoading(true);
|
|
1222
|
-
try {
|
|
1223
|
-
await readForward(RUN_HISTORY_MAX_PAGES);
|
|
1224
|
-
} catch (err) {
|
|
1225
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1226
|
-
} finally {
|
|
1227
|
-
setLoading(false);
|
|
1228
|
-
}
|
|
1229
|
-
}, [readForward]);
|
|
1230
|
-
useEffect(() => {
|
|
1231
|
-
if (!runId || !enabled) {
|
|
1232
|
-
generation.current += 1;
|
|
1233
|
-
cursor.current = 0;
|
|
1234
|
-
setEvents([]);
|
|
1235
|
-
setLoading(false);
|
|
1236
|
-
return;
|
|
1237
|
-
}
|
|
1238
|
-
void refetch();
|
|
1239
|
-
}, [runId, enabled, refetch]);
|
|
1240
|
-
useEffect(() => {
|
|
1241
|
-
const client = getFluidGridsClientSafe();
|
|
1242
|
-
if (!runId || !enabled || !live || !client) {
|
|
1243
|
-
setStreaming(false);
|
|
1244
|
-
return;
|
|
1245
|
-
}
|
|
1246
|
-
let cancelled = false;
|
|
1247
|
-
let poll = null;
|
|
1248
|
-
let safetyRead = null;
|
|
1249
|
-
let delivered = false;
|
|
1250
|
-
const stopSafetyReads = () => {
|
|
1251
|
-
if (safetyRead) clearTimeout(safetyRead);
|
|
1252
|
-
safetyRead = null;
|
|
1253
|
-
};
|
|
1254
|
-
const startPolling = () => {
|
|
1255
|
-
stopSafetyReads();
|
|
1256
|
-
if (poll || cancelled) return;
|
|
1257
|
-
setStreaming(false);
|
|
1258
|
-
poll = setInterval(() => {
|
|
1259
|
-
void readForward(1).catch(() => {
|
|
1260
|
-
});
|
|
1261
|
-
}, RUN_HISTORY_POLL_INTERVAL_MS);
|
|
1262
|
-
};
|
|
1263
|
-
const scheduleSafetyRead = (delay) => {
|
|
1264
|
-
safetyRead = setTimeout(() => {
|
|
1265
|
-
safetyRead = null;
|
|
1266
|
-
if (cancelled || delivered || poll) return;
|
|
1267
|
-
void readForward(1).catch(() => {
|
|
1268
|
-
});
|
|
1269
|
-
scheduleSafetyRead(Math.min(delay * 2, RUN_HISTORY_SAFETY_READ_MAX_MS));
|
|
1270
|
-
}, delay);
|
|
1271
|
-
};
|
|
1272
|
-
const subscription = client.subscribe({ query: RunEventAppendedDocument, variables: { runId } }).subscribe({
|
|
1273
|
-
next: ({ data }) => {
|
|
1274
|
-
if (cancelled || !data?.runEventAppended) return;
|
|
1275
|
-
delivered = true;
|
|
1276
|
-
stopSafetyReads();
|
|
1277
|
-
if (poll) {
|
|
1278
|
-
clearInterval(poll);
|
|
1279
|
-
poll = null;
|
|
1280
|
-
}
|
|
1281
|
-
setStreaming(true);
|
|
1282
|
-
append(toEvents([data.runEventAppended]));
|
|
1283
|
-
},
|
|
1284
|
-
error: () => startPolling(),
|
|
1285
|
-
complete: () => {
|
|
1286
|
-
if (!cancelled) startPolling();
|
|
1287
|
-
}
|
|
1288
|
-
});
|
|
1289
|
-
scheduleSafetyRead(RUN_HISTORY_SAFETY_READ_INITIAL_MS);
|
|
1290
|
-
return () => {
|
|
1291
|
-
cancelled = true;
|
|
1292
|
-
stopSafetyReads();
|
|
1293
|
-
if (poll) clearInterval(poll);
|
|
1294
|
-
subscription.unsubscribe();
|
|
1295
|
-
setStreaming(false);
|
|
1296
|
-
};
|
|
1297
|
-
}, [runId, enabled, live, readForward, append]);
|
|
1298
|
-
const wasLive = useRef(false);
|
|
1299
|
-
useEffect(() => {
|
|
1300
|
-
if (live) {
|
|
1301
|
-
wasLive.current = true;
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
if (!wasLive.current) return;
|
|
1305
|
-
wasLive.current = false;
|
|
1306
|
-
void readForward(RUN_HISTORY_MAX_PAGES).catch(() => {
|
|
1307
|
-
});
|
|
1308
|
-
}, [live, readForward]);
|
|
1309
|
-
return {
|
|
1310
|
-
events,
|
|
1311
|
-
loading,
|
|
1312
|
-
error,
|
|
1313
|
-
truncated,
|
|
1314
|
-
historyLength,
|
|
1315
|
-
streaming,
|
|
1316
|
-
refetch
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
var PROPOSAL_POLL_INTERVAL_MS = 3e3;
|
|
1320
|
-
var PROPOSAL_MAX_WAIT_MS = 6 * 60 * 1e3;
|
|
1321
|
-
function messageOf(error) {
|
|
1322
|
-
return error instanceof Error ? error.message : String(error);
|
|
1323
|
-
}
|
|
1324
|
-
function isProposalRunning(status) {
|
|
1325
|
-
return status === "PROCESSING";
|
|
1326
|
-
}
|
|
1327
|
-
function useWorkflowProposal() {
|
|
1328
|
-
const [proposal, setProposal] = useState(null);
|
|
1329
|
-
const [isWorking, setIsWorking] = useState(false);
|
|
1330
|
-
const [isDeciding, setIsDeciding] = useState(false);
|
|
1331
|
-
const [error, setError] = useState(null);
|
|
1332
|
-
const [timedOut, setTimedOut] = useState(false);
|
|
1333
|
-
const runRef = useRef(0);
|
|
1334
|
-
const timerRef = useRef(null);
|
|
1335
|
-
const clearTimer = useCallback(() => {
|
|
1336
|
-
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1337
|
-
timerRef.current = null;
|
|
1338
|
-
}, []);
|
|
1339
|
-
useEffect(
|
|
1340
|
-
() => () => {
|
|
1341
|
-
runRef.current++;
|
|
1342
|
-
clearTimer();
|
|
1343
|
-
},
|
|
1344
|
-
[clearTimer]
|
|
1345
|
-
);
|
|
1346
|
-
const reset = useCallback(() => {
|
|
1347
|
-
runRef.current++;
|
|
1348
|
-
clearTimer();
|
|
1349
|
-
setProposal(null);
|
|
1350
|
-
setIsWorking(false);
|
|
1351
|
-
setIsDeciding(false);
|
|
1352
|
-
setError(null);
|
|
1353
|
-
setTimedOut(false);
|
|
1354
|
-
}, [clearTimer]);
|
|
1355
|
-
const poll = useCallback(
|
|
1356
|
-
async (run, id, startedAt) => {
|
|
1357
|
-
if (run !== runRef.current) return;
|
|
1358
|
-
try {
|
|
1359
|
-
const result = throwIfResultError(
|
|
1360
|
-
await getFluidGridsClient().query({
|
|
1361
|
-
query: WorkflowProposalDocument,
|
|
1362
|
-
variables: { id },
|
|
1363
|
-
fetchPolicy: "network-only"
|
|
1364
|
-
})
|
|
1365
|
-
);
|
|
1366
|
-
if (run !== runRef.current) return;
|
|
1367
|
-
const next = result.data?.workflowProposal ?? null;
|
|
1368
|
-
if (next) setProposal(next);
|
|
1369
|
-
const status = next ? String(next.status) : void 0;
|
|
1370
|
-
if (isProposalRunning(status)) {
|
|
1371
|
-
if (Date.now() - startedAt < PROPOSAL_MAX_WAIT_MS) {
|
|
1372
|
-
timerRef.current = setTimeout(
|
|
1373
|
-
() => void poll(run, id, startedAt),
|
|
1374
|
-
PROPOSAL_POLL_INTERVAL_MS
|
|
1375
|
-
);
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
setTimedOut(true);
|
|
1379
|
-
} else {
|
|
1380
|
-
notifyFluidGridsAiCreditsUpdated();
|
|
1381
|
-
}
|
|
1382
|
-
setIsWorking(false);
|
|
1383
|
-
} catch (pollError) {
|
|
1384
|
-
if (run !== runRef.current) return;
|
|
1385
|
-
setError(messageOf(pollError));
|
|
1386
|
-
setIsWorking(false);
|
|
1387
|
-
}
|
|
1388
|
-
},
|
|
1389
|
-
[]
|
|
1390
|
-
);
|
|
1391
|
-
const start = useCallback(
|
|
1392
|
-
async (input) => {
|
|
1393
|
-
const run = ++runRef.current;
|
|
1394
|
-
clearTimer();
|
|
1395
|
-
setProposal(null);
|
|
1396
|
-
setError(null);
|
|
1397
|
-
setTimedOut(false);
|
|
1398
|
-
setIsWorking(true);
|
|
1399
|
-
try {
|
|
1400
|
-
const client = getFluidGridsClient();
|
|
1401
|
-
let started;
|
|
1402
|
-
if (input.kind === "runFix") {
|
|
1403
|
-
const result = throwIfResultError(
|
|
1404
|
-
await client.mutate({
|
|
1405
|
-
mutation: ProposeRunFixDocument,
|
|
1406
|
-
variables: {
|
|
1407
|
-
input: {
|
|
1408
|
-
runId: input.runId,
|
|
1409
|
-
...input.prompt ? { prompt: input.prompt } : {}
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
})
|
|
1413
|
-
);
|
|
1414
|
-
started = result.data?.proposeRunFix;
|
|
1415
|
-
} else {
|
|
1416
|
-
const result = throwIfResultError(
|
|
1417
|
-
await client.mutate({
|
|
1418
|
-
mutation: ProposeWorkflowChangeDocument,
|
|
1419
|
-
variables: {
|
|
1420
|
-
input: {
|
|
1421
|
-
prompt: input.prompt,
|
|
1422
|
-
...input.workflowKey ? { workflowKey: input.workflowKey } : {},
|
|
1423
|
-
...input.baseVersion ? { baseVersion: input.baseVersion } : {},
|
|
1424
|
-
...input.template ? { template: input.template } : {}
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
})
|
|
1428
|
-
);
|
|
1429
|
-
started = result.data?.proposeWorkflowChange;
|
|
1430
|
-
}
|
|
1431
|
-
if (!started?.id) throw new Error("The proposal did not start");
|
|
1432
|
-
if (run !== runRef.current) return;
|
|
1433
|
-
setProposal(started);
|
|
1434
|
-
const id = started.id;
|
|
1435
|
-
timerRef.current = setTimeout(
|
|
1436
|
-
() => void poll(run, id, Date.now()),
|
|
1437
|
-
PROPOSAL_POLL_INTERVAL_MS
|
|
1438
|
-
);
|
|
1439
|
-
} catch (startError) {
|
|
1440
|
-
if (run !== runRef.current) return;
|
|
1441
|
-
setError(messageOf(startError));
|
|
1442
|
-
setIsWorking(false);
|
|
1443
|
-
}
|
|
1444
|
-
},
|
|
1445
|
-
[clearTimer, poll]
|
|
1446
|
-
);
|
|
1447
|
-
const open = useCallback(
|
|
1448
|
-
async (id) => {
|
|
1449
|
-
const run = ++runRef.current;
|
|
1450
|
-
clearTimer();
|
|
1451
|
-
setError(null);
|
|
1452
|
-
setTimedOut(false);
|
|
1453
|
-
setIsWorking(true);
|
|
1454
|
-
await poll(run, id, Date.now());
|
|
1455
|
-
},
|
|
1456
|
-
[clearTimer, poll]
|
|
1457
|
-
);
|
|
1458
|
-
const decide = useCallback(
|
|
1459
|
-
async (action) => {
|
|
1460
|
-
const id = proposal?.id;
|
|
1461
|
-
if (!id) return null;
|
|
1462
|
-
const run = runRef.current;
|
|
1463
|
-
setIsDeciding(true);
|
|
1464
|
-
setError(null);
|
|
1465
|
-
try {
|
|
1466
|
-
const client = getFluidGridsClient();
|
|
1467
|
-
let next;
|
|
1468
|
-
if (action === "apply") {
|
|
1469
|
-
const result = throwIfResultError(
|
|
1470
|
-
await client.mutate({ mutation: ApplyWorkflowProposalDocument, variables: { id } })
|
|
1471
|
-
);
|
|
1472
|
-
next = result.data?.applyWorkflowProposal;
|
|
1473
|
-
} else if (action === "reject") {
|
|
1474
|
-
const result = throwIfResultError(
|
|
1475
|
-
await client.mutate({ mutation: RejectWorkflowProposalDocument, variables: { id } })
|
|
1476
|
-
);
|
|
1477
|
-
next = result.data?.rejectWorkflowProposal;
|
|
1478
|
-
} else {
|
|
1479
|
-
clearTimer();
|
|
1480
|
-
const result = throwIfResultError(
|
|
1481
|
-
await client.mutate({ mutation: CancelWorkflowProposalDocument, variables: { id } })
|
|
1482
|
-
);
|
|
1483
|
-
next = result.data?.cancelWorkflowProposal;
|
|
1484
|
-
setIsWorking(false);
|
|
1485
|
-
}
|
|
1486
|
-
if (run === runRef.current && next) setProposal(next);
|
|
1487
|
-
return next ?? null;
|
|
1488
|
-
} catch (decideError) {
|
|
1489
|
-
if (run === runRef.current) setError(messageOf(decideError));
|
|
1490
|
-
return null;
|
|
1491
|
-
} finally {
|
|
1492
|
-
if (run === runRef.current) setIsDeciding(false);
|
|
1493
|
-
}
|
|
1494
|
-
},
|
|
1495
|
-
[clearTimer, proposal?.id]
|
|
1496
|
-
);
|
|
1497
|
-
const apply = useCallback(() => decide("apply"), [decide]);
|
|
1498
|
-
const reject = useCallback(async () => {
|
|
1499
|
-
await decide("reject");
|
|
1500
|
-
}, [decide]);
|
|
1501
|
-
const cancel = useCallback(async () => {
|
|
1502
|
-
await decide("cancel");
|
|
1503
|
-
}, [decide]);
|
|
1504
|
-
return {
|
|
1505
|
-
proposal,
|
|
1506
|
-
isWorking,
|
|
1507
|
-
isDeciding,
|
|
1508
|
-
error,
|
|
1509
|
-
timedOut,
|
|
1510
|
-
start,
|
|
1511
|
-
open,
|
|
1512
|
-
apply,
|
|
1513
|
-
reject,
|
|
1514
|
-
cancel,
|
|
1515
|
-
reset
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
var STATUS_TONE = {
|
|
1519
|
-
PROCESSING: "secondary",
|
|
1520
|
-
PROPOSED: "default",
|
|
1521
|
-
APPLIED: "default",
|
|
1522
|
-
REJECTED: "outline",
|
|
1523
|
-
STALE: "outline",
|
|
1524
|
-
CANCELLED: "outline",
|
|
1525
|
-
FAILED: "destructive"
|
|
1526
|
-
};
|
|
1527
|
-
function edgeLabel(edge) {
|
|
1528
|
-
const from = edge.sourceHandle ? `${edge.source} (${edge.sourceHandle})` : edge.source;
|
|
1529
|
-
return `${from} \u2192 ${edge.target}`;
|
|
1530
|
-
}
|
|
1531
|
-
function ChangeList({
|
|
1532
|
-
title,
|
|
1533
|
-
icon,
|
|
1534
|
-
items,
|
|
1535
|
-
testId
|
|
1536
|
-
}) {
|
|
1537
|
-
if (items.length === 0) return null;
|
|
1538
|
-
return /* @__PURE__ */ jsxs("div", { "data-testid": testId, children: [
|
|
1539
|
-
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary", children: [
|
|
1540
|
-
/* @__PURE__ */ jsx(Icon, { icon, width: 14 }),
|
|
1541
|
-
title,
|
|
1542
|
-
" (",
|
|
1543
|
-
items.length,
|
|
1544
|
-
")"
|
|
1545
|
-
] }),
|
|
1546
|
-
/* @__PURE__ */ jsx("ul", { className: "mt-1 space-y-0.5 text-sm text-text-primary", children: items.map((item) => /* @__PURE__ */ jsx("li", { className: "truncate", children: item }, item)) })
|
|
1547
|
-
] });
|
|
1548
|
-
}
|
|
1549
|
-
function nodeLine(node, withFields) {
|
|
1550
|
-
const base = `${node.label} \xB7 ${node.type}`;
|
|
1551
|
-
return withFields && node.fields.length > 0 ? `${base} \u2014 ${node.fields.join(", ")}` : base;
|
|
1552
|
-
}
|
|
1553
|
-
function visibleErrors(errors, shownMessage) {
|
|
1554
|
-
const shown = normalise(shownMessage ?? "");
|
|
1555
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1556
|
-
const result = [];
|
|
1557
|
-
for (const error of errors) {
|
|
1558
|
-
const key = normalise(error);
|
|
1559
|
-
if (!key || seen.has(key)) continue;
|
|
1560
|
-
seen.add(key);
|
|
1561
|
-
if (shown && shown.includes(key)) continue;
|
|
1562
|
-
result.push(error);
|
|
1563
|
-
}
|
|
1564
|
-
return result;
|
|
1565
|
-
}
|
|
1566
|
-
function normalise(text) {
|
|
1567
|
-
return text.trim().toLowerCase().replace(/^[\s.:;,]+|[\s.:;,]+$/g, "");
|
|
1568
|
-
}
|
|
1569
|
-
function WorkflowProposalCard({
|
|
1570
|
-
proposal,
|
|
1571
|
-
isDeciding = false,
|
|
1572
|
-
onOpenWorkflow,
|
|
1573
|
-
onApply,
|
|
1574
|
-
onReject,
|
|
1575
|
-
onCancel,
|
|
1576
|
-
onRevert
|
|
1577
|
-
}) {
|
|
1578
|
-
const tr = useTr();
|
|
1579
|
-
const status = String(proposal.status);
|
|
1580
|
-
const diff = proposal.diff;
|
|
1581
|
-
const warnings = proposal.validation?.warnings ?? [];
|
|
1582
|
-
const showMessage = Boolean(
|
|
1583
|
-
proposal.message && status !== "PROPOSED" && status !== "PROCESSING" && status !== "APPLIED"
|
|
1584
|
-
);
|
|
1585
|
-
const errors = visibleErrors(
|
|
1586
|
-
proposal.validation?.valid === false ? proposal.validation.errors : [],
|
|
1587
|
-
showMessage ? proposal.message : null
|
|
1588
|
-
);
|
|
1589
|
-
const revertTarget = proposal.baseVersion && proposal.workflowKey && proposal.appliedWorkflowKey === proposal.workflowKey ? { workflowKey: proposal.workflowKey, version: proposal.baseVersion } : null;
|
|
1590
|
-
return /* @__PURE__ */ jsxs(
|
|
1591
|
-
"section",
|
|
1592
|
-
{
|
|
1593
|
-
className: "rounded-lg border border-border-secondary bg-background-secondary p-4",
|
|
1594
|
-
"data-testid": "workflow-proposal-card",
|
|
1595
|
-
"data-status": status,
|
|
1596
|
-
children: [
|
|
1597
|
-
/* @__PURE__ */ jsxs("header", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1598
|
-
/* @__PURE__ */ jsx(
|
|
1599
|
-
Icon,
|
|
1600
|
-
{
|
|
1601
|
-
icon: "mdi:creation-outline",
|
|
1602
|
-
width: 18,
|
|
1603
|
-
className: "text-text-secondary"
|
|
1604
|
-
}
|
|
1605
|
-
),
|
|
1606
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-text-primary", children: proposal.name ?? tr("fluidgrids.ai.proposal.title", "AI proposal") }),
|
|
1607
|
-
/* @__PURE__ */ jsx(
|
|
1608
|
-
Badge,
|
|
1609
|
-
{
|
|
1610
|
-
variant: STATUS_TONE[status] ?? "outline",
|
|
1611
|
-
"data-testid": "workflow-proposal-status",
|
|
1612
|
-
children: status
|
|
1613
|
-
}
|
|
1614
|
-
),
|
|
1615
|
-
proposal.baseVersion && /* @__PURE__ */ jsx("span", { className: "text-xs text-text-secondary", children: tr(
|
|
1616
|
-
"fluidgrids.ai.proposal.against",
|
|
1617
|
-
"against version {{version}}",
|
|
1618
|
-
{
|
|
1619
|
-
version: proposal.baseVersion
|
|
1620
|
-
}
|
|
1621
|
-
) })
|
|
1622
|
-
] }),
|
|
1623
|
-
status === "PROCESSING" && /* @__PURE__ */ jsxs("div", { className: "mt-3 flex items-center gap-2 text-sm text-text-secondary", children: [
|
|
1624
|
-
/* @__PURE__ */ jsx(Icon, { icon: "mdi:loading", width: 16, className: "animate-spin" }),
|
|
1625
|
-
tr("fluidgrids.ai.proposal.working", "Working ({{stage}})\u2026", {
|
|
1626
|
-
stage: proposal.progress?.stage ?? "prompting"
|
|
1627
|
-
}),
|
|
1628
|
-
onCancel && /* @__PURE__ */ jsx(
|
|
1629
|
-
Button$1,
|
|
1630
|
-
{
|
|
1631
|
-
size: "sm",
|
|
1632
|
-
variant: "ghost",
|
|
1633
|
-
onClick: onCancel,
|
|
1634
|
-
disabled: isDeciding,
|
|
1635
|
-
children: tr("fluidgrids.ai.proposal.cancel", "Cancel")
|
|
1636
|
-
}
|
|
1637
|
-
)
|
|
1638
|
-
] }),
|
|
1639
|
-
proposal.explanation && /* @__PURE__ */ jsx(
|
|
1640
|
-
"p",
|
|
1641
|
-
{
|
|
1642
|
-
className: "mt-3 text-sm text-text-primary",
|
|
1643
|
-
"data-testid": "workflow-proposal-explanation",
|
|
1644
|
-
children: proposal.explanation
|
|
1645
|
-
}
|
|
1646
|
-
),
|
|
1647
|
-
showMessage && /* @__PURE__ */ jsx(
|
|
1648
|
-
"p",
|
|
1649
|
-
{
|
|
1650
|
-
className: "mt-2 text-sm text-text-secondary",
|
|
1651
|
-
"data-testid": "workflow-proposal-message",
|
|
1652
|
-
children: proposal.message
|
|
1653
|
-
}
|
|
1654
|
-
),
|
|
1655
|
-
diff && /* @__PURE__ */ jsxs("div", { className: "mt-3 space-y-2", "data-testid": "workflow-proposal-diff", children: [
|
|
1656
|
-
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-text-primary", children: diff.summary }),
|
|
1657
|
-
/* @__PURE__ */ jsx(
|
|
1658
|
-
ChangeList,
|
|
1659
|
-
{
|
|
1660
|
-
testId: "workflow-proposal-added",
|
|
1661
|
-
icon: "mdi:plus-circle-outline",
|
|
1662
|
-
title: tr("fluidgrids.ai.proposal.added", "Added nodes"),
|
|
1663
|
-
items: diff.addedNodes.map((node) => nodeLine(node, false))
|
|
1664
|
-
}
|
|
1665
|
-
),
|
|
1666
|
-
/* @__PURE__ */ jsx(
|
|
1667
|
-
ChangeList,
|
|
1668
|
-
{
|
|
1669
|
-
testId: "workflow-proposal-changed",
|
|
1670
|
-
icon: "mdi:pencil-circle-outline",
|
|
1671
|
-
title: tr("fluidgrids.ai.proposal.changed", "Changed nodes"),
|
|
1672
|
-
items: diff.changedNodes.map((node) => nodeLine(node, true))
|
|
1673
|
-
}
|
|
1674
|
-
),
|
|
1675
|
-
/* @__PURE__ */ jsx(
|
|
1676
|
-
ChangeList,
|
|
1677
|
-
{
|
|
1678
|
-
testId: "workflow-proposal-removed",
|
|
1679
|
-
icon: "mdi:minus-circle-outline",
|
|
1680
|
-
title: tr("fluidgrids.ai.proposal.removed", "Removed nodes"),
|
|
1681
|
-
items: diff.removedNodes.map((node) => nodeLine(node, false))
|
|
1682
|
-
}
|
|
1683
|
-
),
|
|
1684
|
-
/* @__PURE__ */ jsx(
|
|
1685
|
-
ChangeList,
|
|
1686
|
-
{
|
|
1687
|
-
testId: "workflow-proposal-edges",
|
|
1688
|
-
icon: "mdi:vector-line",
|
|
1689
|
-
title: tr(
|
|
1690
|
-
"fluidgrids.ai.proposal.connections",
|
|
1691
|
-
"Connection changes"
|
|
1692
|
-
),
|
|
1693
|
-
items: [
|
|
1694
|
-
...diff.addedEdges.map((edge) => `+ ${edgeLabel(edge)}`),
|
|
1695
|
-
...diff.removedEdges.map((edge) => `\u2212 ${edgeLabel(edge)}`)
|
|
1696
|
-
]
|
|
1697
|
-
}
|
|
1698
|
-
)
|
|
1699
|
-
] }),
|
|
1700
|
-
errors.length > 0 && /* @__PURE__ */ jsx(
|
|
1701
|
-
"ul",
|
|
1702
|
-
{
|
|
1703
|
-
className: "mt-3 space-y-1 text-sm text-[var(--color-status-error-text)]",
|
|
1704
|
-
"data-testid": "workflow-proposal-errors",
|
|
1705
|
-
children: errors.map((message) => /* @__PURE__ */ jsx("li", { children: message }, message))
|
|
1706
|
-
}
|
|
1707
|
-
),
|
|
1708
|
-
warnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-3", "data-testid": "workflow-proposal-warnings", children: [
|
|
1709
|
-
/* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-text-secondary", children: tr("fluidgrids.ai.proposal.todo", "Still to set up") }),
|
|
1710
|
-
/* @__PURE__ */ jsx("ul", { className: "mt-1 space-y-0.5 text-sm text-text-secondary", children: warnings.map((warning) => /* @__PURE__ */ jsx("li", { children: warning }, warning)) })
|
|
1711
|
-
] }),
|
|
1712
|
-
status === "PROPOSED" && (onApply || onReject) && /* @__PURE__ */ jsxs("div", { className: "mt-4 flex flex-wrap items-center gap-2", children: [
|
|
1713
|
-
onApply && /* @__PURE__ */ jsx(
|
|
1714
|
-
Button$1,
|
|
1715
|
-
{
|
|
1716
|
-
size: "sm",
|
|
1717
|
-
onClick: onApply,
|
|
1718
|
-
disabled: isDeciding,
|
|
1719
|
-
"data-testid": "workflow-proposal-apply",
|
|
1720
|
-
children: proposal.baseVersion ? tr(
|
|
1721
|
-
"fluidgrids.ai.proposal.applyVersion",
|
|
1722
|
-
"Apply as a new draft version"
|
|
1723
|
-
) : tr(
|
|
1724
|
-
"fluidgrids.ai.proposal.applyNew",
|
|
1725
|
-
"Create as a draft workflow"
|
|
1726
|
-
)
|
|
1727
|
-
}
|
|
1728
|
-
),
|
|
1729
|
-
onReject && /* @__PURE__ */ jsx(
|
|
1730
|
-
Button$1,
|
|
1731
|
-
{
|
|
1732
|
-
size: "sm",
|
|
1733
|
-
variant: "outline",
|
|
1734
|
-
onClick: onReject,
|
|
1735
|
-
disabled: isDeciding,
|
|
1736
|
-
children: tr("fluidgrids.ai.proposal.reject", "Reject")
|
|
1737
|
-
}
|
|
1738
|
-
),
|
|
1739
|
-
/* @__PURE__ */ jsx("span", { className: "text-xs text-text-secondary", children: tr(
|
|
1740
|
-
"fluidgrids.ai.proposal.neverActivates",
|
|
1741
|
-
"Nothing goes live: the result is a draft you can review, run as a test and activate yourself."
|
|
1742
|
-
) })
|
|
1743
|
-
] }),
|
|
1744
|
-
status === "APPLIED" && proposal.appliedWorkflowKey && proposal.appliedVersion && /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
|
|
1745
|
-
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
|
|
1746
|
-
onOpenWorkflow && /* @__PURE__ */ jsx(
|
|
1747
|
-
Button$1,
|
|
1748
|
-
{
|
|
1749
|
-
size: "sm",
|
|
1750
|
-
variant: "outline",
|
|
1751
|
-
"data-testid": "workflow-proposal-open",
|
|
1752
|
-
onClick: () => onOpenWorkflow(
|
|
1753
|
-
proposal.appliedWorkflowKey ?? "",
|
|
1754
|
-
proposal.appliedVersion ?? ""
|
|
1755
|
-
),
|
|
1756
|
-
children: tr("fluidgrids.ai.proposal.open", "Open draft {{version}}", {
|
|
1757
|
-
version: proposal.appliedVersion
|
|
1758
|
-
})
|
|
1759
|
-
}
|
|
1760
|
-
),
|
|
1761
|
-
onRevert && revertTarget && /* @__PURE__ */ jsx(
|
|
1762
|
-
Button$1,
|
|
1763
|
-
{
|
|
1764
|
-
size: "sm",
|
|
1765
|
-
variant: "ghost",
|
|
1766
|
-
"data-testid": "workflow-proposal-revert",
|
|
1767
|
-
disabled: isDeciding,
|
|
1768
|
-
onClick: () => onRevert(revertTarget.workflowKey, revertTarget.version),
|
|
1769
|
-
children: tr(
|
|
1770
|
-
"fluidgrids.ai.proposal.revert",
|
|
1771
|
-
"Revert to {{version}}",
|
|
1772
|
-
{ version: revertTarget.version }
|
|
1773
|
-
)
|
|
1774
|
-
}
|
|
1775
|
-
)
|
|
1776
|
-
] }),
|
|
1777
|
-
!onRevert && proposal.restoreHint && /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: proposal.restoreHint })
|
|
1778
|
-
] })
|
|
1779
|
-
]
|
|
1780
|
-
}
|
|
1781
|
-
);
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
// src/builder/lib/assert-rules.ts
|
|
1785
|
-
var ASSERT_OPERATORS = [
|
|
1786
|
-
{
|
|
1787
|
-
wire: "equals",
|
|
1788
|
-
value: "EQUALS" /* Equals */,
|
|
1789
|
-
operand: "expected",
|
|
1790
|
-
label: "equals"
|
|
1791
|
-
},
|
|
1792
|
-
{
|
|
1793
|
-
wire: "notEquals",
|
|
1794
|
-
value: "NOT_EQUALS" /* NotEquals */,
|
|
1795
|
-
operand: "expected",
|
|
1796
|
-
label: "does not equal"
|
|
1797
|
-
},
|
|
1798
|
-
{
|
|
1799
|
-
wire: "contains",
|
|
1800
|
-
value: "CONTAINS" /* Contains */,
|
|
1801
|
-
operand: "expected",
|
|
1802
|
-
label: "contains"
|
|
1803
|
-
},
|
|
1804
|
-
{
|
|
1805
|
-
wire: "notContains",
|
|
1806
|
-
value: "NOT_CONTAINS" /* NotContains */,
|
|
1807
|
-
operand: "expected",
|
|
1808
|
-
label: "does not contain"
|
|
1809
|
-
},
|
|
1810
|
-
{
|
|
1811
|
-
wire: "matches",
|
|
1812
|
-
value: "MATCHES" /* Matches */,
|
|
1813
|
-
operand: "expected",
|
|
1814
|
-
label: "matches regex"
|
|
1815
|
-
},
|
|
1816
|
-
{
|
|
1817
|
-
wire: "jsonSchema",
|
|
1818
|
-
value: "JSON_SCHEMA" /* JsonSchema */,
|
|
1819
|
-
operand: "expected",
|
|
1820
|
-
label: "matches JSON Schema"
|
|
1821
|
-
},
|
|
1822
|
-
{
|
|
1823
|
-
wire: "between",
|
|
1824
|
-
value: "BETWEEN" /* Between */,
|
|
1825
|
-
operand: "range",
|
|
1826
|
-
label: "is between"
|
|
1827
|
-
},
|
|
1828
|
-
{
|
|
1829
|
-
wire: "itemCount",
|
|
1830
|
-
value: "ITEM_COUNT" /* ItemCount */,
|
|
1831
|
-
operand: "range",
|
|
1832
|
-
label: "has item count"
|
|
1833
|
-
},
|
|
1834
|
-
{
|
|
1835
|
-
wire: "exists",
|
|
1836
|
-
value: "EXISTS" /* Exists */,
|
|
1837
|
-
operand: "none",
|
|
1838
|
-
label: "exists"
|
|
1839
|
-
},
|
|
1840
|
-
{
|
|
1841
|
-
wire: "notExists",
|
|
1842
|
-
value: "NOT_EXISTS" /* NotExists */,
|
|
1843
|
-
operand: "none",
|
|
1844
|
-
label: "does not exist"
|
|
1845
|
-
}
|
|
1846
|
-
];
|
|
1847
|
-
var MAX_REGEX_LENGTH = 500;
|
|
1848
|
-
var MAX_RULES = 50;
|
|
1849
|
-
function operatorSpec(value) {
|
|
1850
|
-
if (typeof value !== "string") return null;
|
|
1851
|
-
return ASSERT_OPERATORS.find(
|
|
1852
|
-
(spec) => spec.wire === value || spec.value === value
|
|
1853
|
-
) ?? null;
|
|
1854
|
-
}
|
|
1855
|
-
function isRecord2(value) {
|
|
1856
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1857
|
-
}
|
|
1858
|
-
function optionalNumber(value) {
|
|
1859
|
-
if (value === void 0 || value === null) return void 0;
|
|
1860
|
-
return typeof value === "number" && Number.isFinite(value) ? value : false;
|
|
1861
|
-
}
|
|
1862
|
-
function normaliseAssertRule(raw, index) {
|
|
1863
|
-
if (!isRecord2(raw)) return { problem: { code: "notObject", index } };
|
|
1864
|
-
const spec = operatorSpec(raw.operator);
|
|
1865
|
-
if (!spec)
|
|
1866
|
-
return {
|
|
1867
|
-
problem: {
|
|
1868
|
-
code: "unknownOperator",
|
|
1869
|
-
index,
|
|
1870
|
-
operator: String(raw.operator ?? "")
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
const rule = { operator: spec.wire };
|
|
1874
|
-
if (raw.path !== void 0 && raw.path !== null && raw.path !== "") {
|
|
1875
|
-
if (typeof raw.path !== "string")
|
|
1876
|
-
return { problem: { code: "pathInvalid", index } };
|
|
1877
|
-
rule.path = raw.path;
|
|
1878
|
-
}
|
|
1879
|
-
if (raw.message !== void 0 && raw.message !== null && raw.message !== "") {
|
|
1880
|
-
if (typeof raw.message !== "string")
|
|
1881
|
-
return { problem: { code: "messageInvalid", index } };
|
|
1882
|
-
rule.message = raw.message;
|
|
1883
|
-
}
|
|
1884
|
-
if (spec.operand === "expected") {
|
|
1885
|
-
if (!("expected" in raw))
|
|
1886
|
-
return { problem: { code: "expectedRequired", index } };
|
|
1887
|
-
if (spec.wire === "matches") {
|
|
1888
|
-
if (typeof raw.expected !== "string")
|
|
1889
|
-
return { problem: { code: "regexInvalid", index } };
|
|
1890
|
-
if (raw.expected.length > MAX_REGEX_LENGTH)
|
|
1891
|
-
return { problem: { code: "regexTooLong", index } };
|
|
1892
|
-
try {
|
|
1893
|
-
new RegExp(raw.expected);
|
|
1894
|
-
} catch {
|
|
1895
|
-
return { problem: { code: "regexInvalid", index } };
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
if (spec.wire === "jsonSchema" && !isRecord2(raw.expected))
|
|
1899
|
-
return { problem: { code: "schemaInvalid", index } };
|
|
1900
|
-
rule.expected = raw.expected;
|
|
1901
|
-
}
|
|
1902
|
-
if (spec.operand === "range") {
|
|
1903
|
-
const min = optionalNumber(raw.min);
|
|
1904
|
-
const max = optionalNumber(raw.max);
|
|
1905
|
-
if (min === false || max === false)
|
|
1906
|
-
return { problem: { code: "rangeInvalid", index } };
|
|
1907
|
-
if (min === void 0 && max === void 0)
|
|
1908
|
-
return { problem: { code: "rangeRequired", index } };
|
|
1909
|
-
if (min !== void 0 && max !== void 0 && min > max)
|
|
1910
|
-
return { problem: { code: "rangeInvalid", index } };
|
|
1911
|
-
if (min !== void 0) rule.min = min;
|
|
1912
|
-
if (max !== void 0) rule.max = max;
|
|
1913
|
-
}
|
|
1914
|
-
return { rule };
|
|
1915
|
-
}
|
|
1916
|
-
function normaliseAssertRules(raw) {
|
|
1917
|
-
if (raw.length === 0) return { problem: { code: "empty" } };
|
|
1918
|
-
if (raw.length > MAX_RULES)
|
|
1919
|
-
return { problem: { code: "tooMany", count: raw.length } };
|
|
1920
|
-
const rules = [];
|
|
1921
|
-
for (const [index, item] of raw.entries()) {
|
|
1922
|
-
const result = normaliseAssertRule(item, index);
|
|
1923
|
-
if ("problem" in result) return result;
|
|
1924
|
-
rules.push(result.rule);
|
|
1925
|
-
}
|
|
1926
|
-
return { rules };
|
|
1927
|
-
}
|
|
1928
|
-
function describeAssertRuleProblem(problem) {
|
|
1929
|
-
const at = "index" in problem ? `Rule ${problem.index + 1}: ` : "";
|
|
1930
|
-
switch (problem.code) {
|
|
1931
|
-
case "notObject":
|
|
1932
|
-
return {
|
|
1933
|
-
key: "fluidgrids.builder.assertRules.notObject",
|
|
1934
|
-
fallback: `${at}a rule must be an object.`
|
|
1935
|
-
};
|
|
1936
|
-
case "unknownOperator":
|
|
1937
|
-
return {
|
|
1938
|
-
key: "fluidgrids.builder.assertRules.unknownOperator",
|
|
1939
|
-
fallback: `${at}unknown operator "${problem.operator}". Use ${ASSERT_OPERATORS.map((spec) => spec.wire).join(", ")}.`
|
|
1940
|
-
};
|
|
1941
|
-
case "expectedRequired":
|
|
1942
|
-
return {
|
|
1943
|
-
key: "fluidgrids.builder.assertRules.expectedRequired",
|
|
1944
|
-
fallback: `${at}this operator needs an expected value.`
|
|
1945
|
-
};
|
|
1946
|
-
case "rangeRequired":
|
|
1947
|
-
return {
|
|
1948
|
-
key: "fluidgrids.builder.assertRules.rangeRequired",
|
|
1949
|
-
fallback: `${at}set a minimum, a maximum or both.`
|
|
1950
|
-
};
|
|
1951
|
-
case "rangeInvalid":
|
|
1952
|
-
return {
|
|
1953
|
-
key: "fluidgrids.builder.assertRules.rangeInvalid",
|
|
1954
|
-
fallback: `${at}minimum and maximum must be numbers, with the minimum not above the maximum.`
|
|
1955
|
-
};
|
|
1956
|
-
case "regexInvalid":
|
|
1957
|
-
return {
|
|
1958
|
-
key: "fluidgrids.builder.assertRules.regexInvalid",
|
|
1959
|
-
fallback: `${at}the expected value is not a valid regular expression.`
|
|
1960
|
-
};
|
|
1961
|
-
case "regexTooLong":
|
|
1962
|
-
return {
|
|
1963
|
-
key: "fluidgrids.builder.assertRules.regexTooLong",
|
|
1964
|
-
fallback: `${at}a regular expression may have at most ${MAX_REGEX_LENGTH} characters.`
|
|
1965
|
-
};
|
|
1966
|
-
case "schemaInvalid":
|
|
1967
|
-
return {
|
|
1968
|
-
key: "fluidgrids.builder.assertRules.schemaInvalid",
|
|
1969
|
-
fallback: `${at}the expected value must be a JSON Schema object.`
|
|
1970
|
-
};
|
|
1971
|
-
case "pathInvalid":
|
|
1972
|
-
return {
|
|
1973
|
-
key: "fluidgrids.builder.assertRules.pathInvalid",
|
|
1974
|
-
fallback: `${at}the path must be text.`
|
|
1975
|
-
};
|
|
1976
|
-
case "messageInvalid":
|
|
1977
|
-
return {
|
|
1978
|
-
key: "fluidgrids.builder.assertRules.messageInvalid",
|
|
1979
|
-
fallback: `${at}the message must be text.`
|
|
1980
|
-
};
|
|
1981
|
-
case "tooMany":
|
|
1982
|
-
return {
|
|
1983
|
-
key: "fluidgrids.builder.assertRules.tooMany",
|
|
1984
|
-
fallback: `At most ${MAX_RULES} rules (found ${problem.count}).`
|
|
1985
|
-
};
|
|
1986
|
-
case "empty":
|
|
1987
|
-
return {
|
|
1988
|
-
key: "fluidgrids.builder.assertRules.empty",
|
|
1989
|
-
fallback: "Add at least one rule."
|
|
1990
|
-
};
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
function readAssertRulesSetting(value) {
|
|
1994
|
-
if (value === void 0 || value === null || value === "") return [];
|
|
1995
|
-
if (Array.isArray(value)) return value;
|
|
1996
|
-
if (typeof value !== "string") return null;
|
|
1997
|
-
try {
|
|
1998
|
-
const parsed = JSON.parse(value);
|
|
1999
|
-
return Array.isArray(parsed) ? parsed : null;
|
|
2000
|
-
} catch {
|
|
2001
|
-
return null;
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
function emptyAssertRuleRow() {
|
|
2005
|
-
return {
|
|
2006
|
-
path: "",
|
|
2007
|
-
operator: "equals",
|
|
2008
|
-
expected: "",
|
|
2009
|
-
min: "",
|
|
2010
|
-
max: "",
|
|
2011
|
-
message: ""
|
|
2012
|
-
};
|
|
2013
|
-
}
|
|
2014
|
-
function rowNumber(value) {
|
|
2015
|
-
return typeof value === "number" && Number.isFinite(value) ? String(value) : "";
|
|
2016
|
-
}
|
|
2017
|
-
function rowExpected(operator, value) {
|
|
2018
|
-
if (value === void 0) return "";
|
|
2019
|
-
if (operator === "matches" && typeof value === "string") return value;
|
|
2020
|
-
return JSON.stringify(value);
|
|
2021
|
-
}
|
|
2022
|
-
function rowsFromRules(rules) {
|
|
2023
|
-
return rules.map((raw) => {
|
|
2024
|
-
const rule = isRecord2(raw) ? raw : {};
|
|
2025
|
-
const operator = operatorSpec(rule.operator)?.wire ?? (typeof rule.operator === "string" ? rule.operator : "equals");
|
|
2026
|
-
return {
|
|
2027
|
-
path: typeof rule.path === "string" ? rule.path : "",
|
|
2028
|
-
operator,
|
|
2029
|
-
expected: rowExpected(operator, rule.expected),
|
|
2030
|
-
min: rowNumber(rule.min),
|
|
2031
|
-
max: rowNumber(rule.max),
|
|
2032
|
-
message: typeof rule.message === "string" ? rule.message : ""
|
|
2033
|
-
};
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
function parseExpected(operator, text) {
|
|
2037
|
-
if (operator === "matches") return text;
|
|
2038
|
-
try {
|
|
2039
|
-
return JSON.parse(text);
|
|
2040
|
-
} catch {
|
|
2041
|
-
return text;
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
function parseRowNumber(text) {
|
|
2045
|
-
const trimmed = text.trim();
|
|
2046
|
-
if (trimmed === "") return void 0;
|
|
2047
|
-
const value = Number(trimmed);
|
|
2048
|
-
return Number.isFinite(value) ? value : trimmed;
|
|
2049
|
-
}
|
|
2050
|
-
function ruleFromRow(row) {
|
|
2051
|
-
const spec = operatorSpec(row.operator);
|
|
2052
|
-
const rule = { operator: row.operator };
|
|
2053
|
-
if (row.path.trim()) rule.path = row.path.trim();
|
|
2054
|
-
if (spec?.operand === "expected" && (row.expected !== "" || spec.wire === "matches"))
|
|
2055
|
-
rule.expected = parseExpected(spec.wire, row.expected);
|
|
2056
|
-
if (spec?.operand === "range") {
|
|
2057
|
-
const min = parseRowNumber(row.min);
|
|
2058
|
-
const max = parseRowNumber(row.max);
|
|
2059
|
-
if (min !== void 0) rule.min = min;
|
|
2060
|
-
if (max !== void 0) rule.max = max;
|
|
2061
|
-
}
|
|
2062
|
-
if (row.message.trim()) rule.message = row.message.trim();
|
|
2063
|
-
return rule;
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
// src/builder/lib/workflow-tests.ts
|
|
2067
|
-
var MAX_TEST_CASES = 50;
|
|
2068
|
-
var MAX_CASE_JSON_BYTES = 256 * 1024;
|
|
2069
|
-
var MAX_CASE_ENTRIES = 100;
|
|
2070
|
-
var MAX_CASE_NAME = 200;
|
|
2071
|
-
function emptyTestCaseDraft() {
|
|
2072
|
-
return {
|
|
2073
|
-
name: "",
|
|
2074
|
-
description: "",
|
|
2075
|
-
enabled: true,
|
|
2076
|
-
expectedStatus: "COMPLETED" /* Completed */,
|
|
2077
|
-
triggerNodeId: "",
|
|
2078
|
-
triggerData: "{}",
|
|
2079
|
-
pinnedOutputs: "{}",
|
|
2080
|
-
connectionMocks: "[]",
|
|
2081
|
-
expectations: "{}"
|
|
2082
|
-
};
|
|
2083
|
-
}
|
|
2084
|
-
function pretty(value) {
|
|
2085
|
-
return JSON.stringify(value, null, 2);
|
|
2086
|
-
}
|
|
2087
|
-
function isRecord3(value) {
|
|
2088
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2089
|
-
}
|
|
2090
|
-
function wireRule(rule) {
|
|
2091
|
-
const wire = {
|
|
2092
|
-
operator: operatorSpec(rule.operator)?.wire ?? String(rule.operator)
|
|
2093
|
-
};
|
|
2094
|
-
if (rule.path) wire.path = rule.path;
|
|
2095
|
-
if (rule.expected !== null && rule.expected !== void 0)
|
|
2096
|
-
wire.expected = rule.expected;
|
|
2097
|
-
if (rule.min !== null && rule.min !== void 0) wire.min = rule.min;
|
|
2098
|
-
if (rule.max !== null && rule.max !== void 0) wire.max = rule.max;
|
|
2099
|
-
if (rule.message) wire.message = rule.message;
|
|
2100
|
-
return wire;
|
|
2101
|
-
}
|
|
2102
|
-
function draftFromTestCase(testCase) {
|
|
2103
|
-
const expectations = {};
|
|
2104
|
-
for (const expectation of testCase.expectations)
|
|
2105
|
-
expectations[expectation.nodeId] = expectation.rules.map(wireRule);
|
|
2106
|
-
const mocks = testCase.connectionMocks.map((mock) => {
|
|
2107
|
-
const entry = { nodeId: mock.nodeId };
|
|
2108
|
-
if (mock.operation) entry.operation = mock.operation;
|
|
2109
|
-
if (mock.error) {
|
|
2110
|
-
entry.error = mock.error.errorType ? { message: mock.error.message, errorType: mock.error.errorType } : { message: mock.error.message };
|
|
2111
|
-
} else {
|
|
2112
|
-
entry.response = mock.response ?? {};
|
|
2113
|
-
}
|
|
2114
|
-
return entry;
|
|
2115
|
-
});
|
|
2116
|
-
return {
|
|
2117
|
-
name: testCase.name,
|
|
2118
|
-
description: testCase.description ?? "",
|
|
2119
|
-
enabled: testCase.enabled,
|
|
2120
|
-
expectedStatus: testCase.expectedStatus,
|
|
2121
|
-
triggerNodeId: testCase.triggerNodeId ?? "",
|
|
2122
|
-
triggerData: pretty(testCase.triggerData ?? {}),
|
|
2123
|
-
pinnedOutputs: pretty(testCase.pinnedOutputs ?? {}),
|
|
2124
|
-
connectionMocks: pretty(mocks),
|
|
2125
|
-
expectations: pretty(expectations)
|
|
2126
|
-
};
|
|
2127
|
-
}
|
|
2128
|
-
function pinnedOutputsFromCanvas(pinned) {
|
|
2129
|
-
const outputs = {};
|
|
2130
|
-
for (const [nodeId, entry] of Object.entries(pinned ?? {})) {
|
|
2131
|
-
if (entry && isRecord3(entry.data)) outputs[nodeId] = { data: entry.data };
|
|
2132
|
-
}
|
|
2133
|
-
return outputs;
|
|
2134
|
-
}
|
|
2135
|
-
function parseJson(text, field, label) {
|
|
2136
|
-
const source = text.trim();
|
|
2137
|
-
if (source === "") return { value: void 0 };
|
|
2138
|
-
try {
|
|
2139
|
-
return { value: JSON.parse(source) };
|
|
2140
|
-
} catch (err) {
|
|
2141
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
2142
|
-
return {
|
|
2143
|
-
error: {
|
|
2144
|
-
field,
|
|
2145
|
-
key: "fluidgrids.builder.workflowTests.invalidJson",
|
|
2146
|
-
fallback: `${label} is not valid JSON: ${reason}`
|
|
2147
|
-
}
|
|
2148
|
-
};
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2151
|
-
function fieldError(field, key, fallback) {
|
|
2152
|
-
return {
|
|
2153
|
-
error: { field, key: `fluidgrids.builder.workflowTests.${key}`, fallback }
|
|
2154
|
-
};
|
|
2155
|
-
}
|
|
2156
|
-
function parseTriggerData(text) {
|
|
2157
|
-
const parsed = parseJson(text, "triggerData", "Trigger data");
|
|
2158
|
-
if ("error" in parsed) return parsed;
|
|
2159
|
-
if (parsed.value === void 0) return { value: {} };
|
|
2160
|
-
if (!isRecord3(parsed.value))
|
|
2161
|
-
return fieldError(
|
|
2162
|
-
"triggerData",
|
|
2163
|
-
"triggerDataShape",
|
|
2164
|
-
"Trigger data must be a JSON object."
|
|
2165
|
-
);
|
|
2166
|
-
return { value: parsed.value };
|
|
2167
|
-
}
|
|
2168
|
-
function parsePinnedOutputs(text) {
|
|
2169
|
-
const parsed = parseJson(text, "pinnedOutputs", "Pinned outputs");
|
|
2170
|
-
if ("error" in parsed) return parsed;
|
|
2171
|
-
if (parsed.value === void 0) return { value: {} };
|
|
2172
|
-
if (!isRecord3(parsed.value))
|
|
2173
|
-
return fieldError(
|
|
2174
|
-
"pinnedOutputs",
|
|
2175
|
-
"pinnedShape",
|
|
2176
|
-
'Pinned outputs must be an object of { "nodeId": { "data": { ... } } }.'
|
|
2177
|
-
);
|
|
2178
|
-
const entries = Object.entries(parsed.value);
|
|
2179
|
-
if (entries.length > MAX_CASE_ENTRIES)
|
|
2180
|
-
return fieldError(
|
|
2181
|
-
"pinnedOutputs",
|
|
2182
|
-
"pinnedTooMany",
|
|
2183
|
-
`At most ${MAX_CASE_ENTRIES} pinned nodes.`
|
|
2184
|
-
);
|
|
2185
|
-
const outputs = {};
|
|
2186
|
-
for (const [nodeId, entry] of entries) {
|
|
2187
|
-
if (!isRecord3(entry) || !isRecord3(entry.data))
|
|
2188
|
-
return fieldError(
|
|
2189
|
-
"pinnedOutputs",
|
|
2190
|
-
"pinnedShape",
|
|
2191
|
-
`Pinned output "${nodeId}" must be { "data": { ... } }.`
|
|
2192
|
-
);
|
|
2193
|
-
outputs[nodeId] = { data: entry.data };
|
|
2194
|
-
}
|
|
2195
|
-
return { value: outputs };
|
|
2196
|
-
}
|
|
2197
|
-
var ERROR_TYPES = new Set(Object.values(ErrorType));
|
|
2198
|
-
function isErrorType(value) {
|
|
2199
|
-
return ERROR_TYPES.has(value);
|
|
2200
|
-
}
|
|
2201
|
-
function parseConnectionMocks(text) {
|
|
2202
|
-
const parsed = parseJson(text, "connectionMocks", "Connection mocks");
|
|
2203
|
-
if ("error" in parsed) return parsed;
|
|
2204
|
-
if (parsed.value === void 0) return { value: [] };
|
|
2205
|
-
if (!Array.isArray(parsed.value))
|
|
2206
|
-
return fieldError(
|
|
2207
|
-
"connectionMocks",
|
|
2208
|
-
"mocksShape",
|
|
2209
|
-
'Connection mocks must be an array of { "nodeId", "response" } or { "nodeId", "error" }.'
|
|
2210
|
-
);
|
|
2211
|
-
if (parsed.value.length > MAX_CASE_ENTRIES)
|
|
2212
|
-
return fieldError(
|
|
2213
|
-
"connectionMocks",
|
|
2214
|
-
"mocksTooMany",
|
|
2215
|
-
`At most ${MAX_CASE_ENTRIES} connection mocks.`
|
|
2216
|
-
);
|
|
2217
|
-
const mocks = [];
|
|
2218
|
-
for (const [index, raw] of parsed.value.entries()) {
|
|
2219
|
-
const at = `Mock ${index + 1}`;
|
|
2220
|
-
if (!isRecord3(raw) || typeof raw.nodeId !== "string" || !raw.nodeId)
|
|
2221
|
-
return fieldError(
|
|
2222
|
-
"connectionMocks",
|
|
2223
|
-
"mockNodeRequired",
|
|
2224
|
-
`${at} needs a "nodeId".`
|
|
2225
|
-
);
|
|
2226
|
-
if (raw.operation !== void 0 && raw.operation !== null && typeof raw.operation !== "string")
|
|
2227
|
-
return fieldError(
|
|
2228
|
-
"connectionMocks",
|
|
2229
|
-
"mockOperationInvalid",
|
|
2230
|
-
`${at}: "operation" must be text.`
|
|
2231
|
-
);
|
|
2232
|
-
const hasResponse = raw.response !== void 0 && raw.response !== null;
|
|
2233
|
-
const hasError = raw.error !== void 0 && raw.error !== null;
|
|
2234
|
-
if (hasResponse === hasError)
|
|
2235
|
-
return fieldError(
|
|
2236
|
-
"connectionMocks",
|
|
2237
|
-
"mockResponseOrError",
|
|
2238
|
-
`${at} must set exactly one of "response" and "error".`
|
|
2239
|
-
);
|
|
2240
|
-
const mock = { nodeId: raw.nodeId };
|
|
2241
|
-
if (typeof raw.operation === "string" && raw.operation)
|
|
2242
|
-
mock.operation = raw.operation;
|
|
2243
|
-
if (hasResponse) {
|
|
2244
|
-
mock.response = jsonScalar(raw.response);
|
|
2245
|
-
} else {
|
|
2246
|
-
const error = raw.error;
|
|
2247
|
-
if (!isRecord3(error) || typeof error.message !== "string")
|
|
2248
|
-
return fieldError(
|
|
2249
|
-
"connectionMocks",
|
|
2250
|
-
"mockErrorShape",
|
|
2251
|
-
`${at}: "error" must be { "message": "...", "errorType"?: "..." }.`
|
|
2252
|
-
);
|
|
2253
|
-
const errorType = error.errorType;
|
|
2254
|
-
if (errorType !== void 0 && errorType !== null) {
|
|
2255
|
-
if (typeof errorType !== "string" || !isErrorType(errorType))
|
|
2256
|
-
return fieldError(
|
|
2257
|
-
"connectionMocks",
|
|
2258
|
-
"mockErrorType",
|
|
2259
|
-
`${at}: "errorType" must be one of ${[...ERROR_TYPES].join(", ")}.`
|
|
2260
|
-
);
|
|
2261
|
-
mock.error = { message: error.message, errorType };
|
|
2262
|
-
} else {
|
|
2263
|
-
mock.error = { message: error.message };
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
mocks.push(mock);
|
|
2267
|
-
}
|
|
2268
|
-
return { value: mocks };
|
|
2269
|
-
}
|
|
2270
|
-
function jsonScalar(value) {
|
|
2271
|
-
return value;
|
|
2272
|
-
}
|
|
2273
|
-
function ruleInput(rule) {
|
|
2274
|
-
const spec = operatorSpec(rule.operator);
|
|
2275
|
-
if (!spec) return [];
|
|
2276
|
-
const input = { operator: spec.value };
|
|
2277
|
-
if (rule.path !== void 0) input.path = rule.path;
|
|
2278
|
-
if (rule.expected !== void 0) input.expected = jsonScalar(rule.expected);
|
|
2279
|
-
if (rule.min !== void 0) input.min = rule.min;
|
|
2280
|
-
if (rule.max !== void 0) input.max = rule.max;
|
|
2281
|
-
if (rule.message !== void 0) input.message = rule.message;
|
|
2282
|
-
return [input];
|
|
2283
|
-
}
|
|
2284
|
-
function expectationEntries(value) {
|
|
2285
|
-
if (isRecord3(value)) return Object.entries(value);
|
|
2286
|
-
if (Array.isArray(value)) {
|
|
2287
|
-
const entries = [];
|
|
2288
|
-
for (const item of value) {
|
|
2289
|
-
if (!isRecord3(item)) return null;
|
|
2290
|
-
entries.push([item.nodeId, item.rules]);
|
|
2291
|
-
}
|
|
2292
|
-
return entries;
|
|
2293
|
-
}
|
|
2294
|
-
return null;
|
|
2295
|
-
}
|
|
2296
|
-
function parseExpectations(text) {
|
|
2297
|
-
const parsed = parseJson(text, "expectations", "Expectations");
|
|
2298
|
-
if ("error" in parsed) return parsed;
|
|
2299
|
-
if (parsed.value === void 0) return { value: [] };
|
|
2300
|
-
const entries = expectationEntries(parsed.value);
|
|
2301
|
-
if (!entries)
|
|
2302
|
-
return fieldError(
|
|
2303
|
-
"expectations",
|
|
2304
|
-
"expectationsShape",
|
|
2305
|
-
'Expectations must be an object of { "nodeId": [rules] }.'
|
|
2306
|
-
);
|
|
2307
|
-
if (entries.length > MAX_CASE_ENTRIES)
|
|
2308
|
-
return fieldError(
|
|
2309
|
-
"expectations",
|
|
2310
|
-
"expectationsTooMany",
|
|
2311
|
-
`At most ${MAX_CASE_ENTRIES} checked nodes.`
|
|
2312
|
-
);
|
|
2313
|
-
const expectations = [];
|
|
2314
|
-
for (const [nodeId, rules] of entries) {
|
|
2315
|
-
if (typeof nodeId !== "string" || !nodeId || !Array.isArray(rules))
|
|
2316
|
-
return fieldError(
|
|
2317
|
-
"expectations",
|
|
2318
|
-
"expectationsShape",
|
|
2319
|
-
'Expectations must be an object of { "nodeId": [rules] }.'
|
|
2320
|
-
);
|
|
2321
|
-
const result = normaliseAssertRules(rules);
|
|
2322
|
-
if ("problem" in result) {
|
|
2323
|
-
const problem = describeAssertRuleProblem(result.problem);
|
|
2324
|
-
return {
|
|
2325
|
-
error: {
|
|
2326
|
-
field: "expectations",
|
|
2327
|
-
key: problem.key,
|
|
2328
|
-
fallback: `${nodeId}: ${problem.fallback}`
|
|
2329
|
-
}
|
|
2330
|
-
};
|
|
2331
|
-
}
|
|
2332
|
-
expectations.push({ nodeId, rules: result.rules.flatMap(ruleInput) });
|
|
2333
|
-
}
|
|
2334
|
-
return { value: expectations };
|
|
2335
|
-
}
|
|
2336
|
-
function draftToInput(draft, sortOrder) {
|
|
2337
|
-
const name = draft.name.trim();
|
|
2338
|
-
if (!name)
|
|
2339
|
-
return fieldError("name", "nameRequired", "Give the test case a name.");
|
|
2340
|
-
if (name.length > MAX_CASE_NAME)
|
|
2341
|
-
return fieldError(
|
|
2342
|
-
"name",
|
|
2343
|
-
"nameTooLong",
|
|
2344
|
-
`A name may have at most ${MAX_CASE_NAME} characters.`
|
|
2345
|
-
);
|
|
2346
|
-
const triggerData = parseTriggerData(draft.triggerData);
|
|
2347
|
-
if ("error" in triggerData) return triggerData;
|
|
2348
|
-
const pinnedOutputs = parsePinnedOutputs(draft.pinnedOutputs);
|
|
2349
|
-
if ("error" in pinnedOutputs) return pinnedOutputs;
|
|
2350
|
-
const connectionMocks = parseConnectionMocks(draft.connectionMocks);
|
|
2351
|
-
if ("error" in connectionMocks) return connectionMocks;
|
|
2352
|
-
const expectations = parseExpectations(draft.expectations);
|
|
2353
|
-
if ("error" in expectations) return expectations;
|
|
2354
|
-
const bytes = new TextEncoder().encode(
|
|
2355
|
-
JSON.stringify([
|
|
2356
|
-
triggerData.value,
|
|
2357
|
-
pinnedOutputs.value,
|
|
2358
|
-
connectionMocks.value,
|
|
2359
|
-
expectations.value
|
|
2360
|
-
])
|
|
2361
|
-
).length;
|
|
2362
|
-
if (bytes > MAX_CASE_JSON_BYTES)
|
|
2363
|
-
return fieldError(
|
|
2364
|
-
"size",
|
|
2365
|
-
"tooLarge",
|
|
2366
|
-
`The case holds ${Math.ceil(bytes / 1024)} KiB of JSON; the limit is ${MAX_CASE_JSON_BYTES / 1024} KiB.`
|
|
2367
|
-
);
|
|
2368
|
-
const description = draft.description.trim();
|
|
2369
|
-
const input = {
|
|
2370
|
-
name,
|
|
2371
|
-
description: description || null,
|
|
2372
|
-
triggerNodeId: draft.triggerNodeId || null,
|
|
2373
|
-
triggerData: triggerData.value,
|
|
2374
|
-
pinnedOutputs: pinnedOutputs.value,
|
|
2375
|
-
connectionMocks: connectionMocks.value,
|
|
2376
|
-
expectedStatus: draft.expectedStatus,
|
|
2377
|
-
expectations: expectations.value,
|
|
2378
|
-
enabled: draft.enabled
|
|
2379
|
-
};
|
|
2380
|
-
if (sortOrder !== void 0) input.sortOrder = sortOrder;
|
|
2381
|
-
return { input };
|
|
2382
|
-
}
|
|
2383
|
-
function isTestRunOpen(status) {
|
|
2384
|
-
return status === "PENDING" /* Pending */ || status === "RUNNING" /* Running */;
|
|
2385
|
-
}
|
|
2386
|
-
function testStatusTone(status) {
|
|
2387
|
-
switch (status) {
|
|
2388
|
-
case "PASSED" /* Passed */:
|
|
2389
|
-
return "success";
|
|
2390
|
-
case "FAILED" /* Failed */:
|
|
2391
|
-
return "error";
|
|
2392
|
-
case "ERROR" /* Error */:
|
|
2393
|
-
return "warning";
|
|
2394
|
-
default:
|
|
2395
|
-
return "pending";
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
var TEST_STATUS_TONE_CLASS = {
|
|
2399
|
-
success: "border-[var(--color-status-success-border)] bg-[var(--color-status-success-bg-subtle)] text-[var(--color-status-success-text)]",
|
|
2400
|
-
error: "border-[var(--color-status-error-border)] bg-[var(--color-status-error-bg-subtle)] text-[var(--color-status-error-text)]",
|
|
2401
|
-
warning: "border-[var(--color-status-warning-border)] bg-[var(--color-status-warning-bg-subtle)] text-[var(--color-status-warning-text)]",
|
|
2402
|
-
pending: "border-border bg-muted text-muted-foreground"
|
|
2403
|
-
};
|
|
2404
|
-
function testStatusLabel(status) {
|
|
2405
|
-
const fallback = {
|
|
2406
|
-
["PENDING" /* Pending */]: "Pending",
|
|
2407
|
-
["RUNNING" /* Running */]: "Running",
|
|
2408
|
-
["PASSED" /* Passed */]: "Passed",
|
|
2409
|
-
["FAILED" /* Failed */]: "Failed",
|
|
2410
|
-
["ERROR" /* Error */]: "Error"
|
|
2411
|
-
};
|
|
2412
|
-
return {
|
|
2413
|
-
key: `fluidgrids.builder.workflowTests.status.${status.toLowerCase()}`,
|
|
2414
|
-
fallback: fallback[status]
|
|
2415
|
-
};
|
|
2416
|
-
}
|
|
2417
|
-
function testRunSummaryText(summary) {
|
|
2418
|
-
const parts = [`${summary.passed} passed`];
|
|
2419
|
-
if (summary.failed) parts.push(`${summary.failed} failed`);
|
|
2420
|
-
if (summary.errored) parts.push(`${summary.errored} errored`);
|
|
2421
|
-
if (summary.pending) parts.push(`${summary.pending} running`);
|
|
2422
|
-
return `${parts.join(" \xB7 ")} of ${summary.total}`;
|
|
2423
|
-
}
|
|
2424
|
-
function enabledCaseIds(cases) {
|
|
2425
|
-
return cases.filter((testCase) => testCase.enabled).map((c) => c.id);
|
|
2426
|
-
}
|
|
2427
|
-
function nextSortOrder(cases) {
|
|
2428
|
-
return cases.reduce((max, c) => Math.max(max, c.sortOrder + 1), 0);
|
|
2429
|
-
}
|
|
2430
|
-
var SAVEABLE_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
2431
|
-
"COMPLETED",
|
|
2432
|
-
"FAILED"
|
|
2433
|
-
]);
|
|
2434
|
-
function canSaveRunAsTestCase(run) {
|
|
2435
|
-
return SAVEABLE_RUN_STATUSES.has(run.status) && !!run.workflowKey && !run.isDurable;
|
|
2436
|
-
}
|
|
2437
|
-
var TEST_RUN_POLL_INTERVAL_MS = 2e3;
|
|
2438
|
-
var TEST_RUN_HISTORY_LIMIT = 10;
|
|
2439
|
-
function messageOf2(error) {
|
|
2440
|
-
return error instanceof Error ? error.message : String(error);
|
|
2441
|
-
}
|
|
2442
|
-
function upsertRun(runs, run) {
|
|
2443
|
-
return [run, ...runs.filter((item) => item.id !== run.id)].slice(
|
|
2444
|
-
0,
|
|
2445
|
-
TEST_RUN_HISTORY_LIMIT
|
|
2446
|
-
);
|
|
2447
|
-
}
|
|
2448
|
-
function useWorkflowTests(workflowKey, enabled) {
|
|
2449
|
-
const [cases, setCases] = useState([]);
|
|
2450
|
-
const [runs, setRuns] = useState([]);
|
|
2451
|
-
const [activeRunId, setActiveRunId] = useState(null);
|
|
2452
|
-
const [loading, setLoading] = useState(false);
|
|
2453
|
-
const [error, setError] = useState(null);
|
|
2454
|
-
const [forbidden, setForbidden] = useState(false);
|
|
2455
|
-
const [pollTick, setPollTick] = useState(0);
|
|
2456
|
-
const keyRef = useRef(workflowKey);
|
|
2457
|
-
const refetch = useCallback(async () => {
|
|
2458
|
-
if (!enabled || !workflowKey) return;
|
|
2459
|
-
setLoading(true);
|
|
2460
|
-
setError(null);
|
|
2461
|
-
try {
|
|
2462
|
-
const client = getFluidGridsClient();
|
|
2463
|
-
const [caseResult, runResult] = await Promise.all([
|
|
2464
|
-
client.query({
|
|
2465
|
-
query: WorkflowTestCasesDocument,
|
|
2466
|
-
variables: { workflowKey },
|
|
2467
|
-
fetchPolicy: "network-only"
|
|
2468
|
-
}),
|
|
2469
|
-
client.query({
|
|
2470
|
-
query: WorkflowTestRunsDocument,
|
|
2471
|
-
variables: { workflowKey, limit: TEST_RUN_HISTORY_LIMIT },
|
|
2472
|
-
fetchPolicy: "network-only"
|
|
2473
|
-
})
|
|
2474
|
-
]);
|
|
2475
|
-
throwIfResultError(caseResult);
|
|
2476
|
-
throwIfResultError(runResult);
|
|
2477
|
-
if (keyRef.current !== workflowKey) return;
|
|
2478
|
-
setCases(caseResult.data?.workflowTestCases ?? []);
|
|
2479
|
-
setRuns(runResult.data?.workflowTestRuns ?? []);
|
|
2480
|
-
setForbidden(false);
|
|
2481
|
-
} catch (err) {
|
|
2482
|
-
if (keyRef.current !== workflowKey) return;
|
|
2483
|
-
if (isForbiddenError(err)) setForbidden(true);
|
|
2484
|
-
else setError(messageOf2(err));
|
|
2485
|
-
} finally {
|
|
2486
|
-
if (keyRef.current === workflowKey) setLoading(false);
|
|
2487
|
-
}
|
|
2488
|
-
}, [enabled, workflowKey]);
|
|
2489
|
-
useEffect(() => {
|
|
2490
|
-
keyRef.current = workflowKey;
|
|
2491
|
-
setCases([]);
|
|
2492
|
-
setRuns([]);
|
|
2493
|
-
setActiveRunId(null);
|
|
2494
|
-
setForbidden(false);
|
|
2495
|
-
setError(null);
|
|
2496
|
-
}, [workflowKey]);
|
|
2497
|
-
const activeRun = (activeRunId ? runs.find((run) => run.id === activeRunId) : runs[0]) ?? null;
|
|
2498
|
-
const followedId = activeRun && isTestRunOpen(activeRun.status) ? activeRun.id : null;
|
|
2499
|
-
useEffect(() => {
|
|
2500
|
-
if (!enabled || !followedId) return;
|
|
2501
|
-
let cancelled = false;
|
|
2502
|
-
const key = workflowKey;
|
|
2503
|
-
const timer = setTimeout(() => {
|
|
2504
|
-
void (async () => {
|
|
2505
|
-
try {
|
|
2506
|
-
const result = await getFluidGridsClient().query({
|
|
2507
|
-
query: WorkflowTestRunDocument,
|
|
2508
|
-
variables: { id: followedId },
|
|
2509
|
-
fetchPolicy: "network-only"
|
|
2510
|
-
});
|
|
2511
|
-
throwIfResultError(result);
|
|
2512
|
-
const run = result.data?.workflowTestRun;
|
|
2513
|
-
if (cancelled || keyRef.current !== key || !run) return;
|
|
2514
|
-
setRuns((current) => upsertRun(current, run));
|
|
2515
|
-
if (!isTestRunOpen(run.status)) await refetchCases(key);
|
|
2516
|
-
} catch (err) {
|
|
2517
|
-
if (!cancelled && keyRef.current === key) setError(messageOf2(err));
|
|
2518
|
-
} finally {
|
|
2519
|
-
if (!cancelled) setPollTick((tick) => tick + 1);
|
|
2520
|
-
}
|
|
2521
|
-
})();
|
|
2522
|
-
}, TEST_RUN_POLL_INTERVAL_MS);
|
|
2523
|
-
async function refetchCases(forKey) {
|
|
2524
|
-
const result = await getFluidGridsClient().query({
|
|
2525
|
-
query: WorkflowTestCasesDocument,
|
|
2526
|
-
variables: { workflowKey: forKey },
|
|
2527
|
-
fetchPolicy: "network-only"
|
|
2528
|
-
});
|
|
2529
|
-
throwIfResultError(result);
|
|
2530
|
-
if (!cancelled && keyRef.current === forKey)
|
|
2531
|
-
setCases(result.data?.workflowTestCases ?? []);
|
|
2532
|
-
}
|
|
2533
|
-
return () => {
|
|
2534
|
-
cancelled = true;
|
|
2535
|
-
clearTimeout(timer);
|
|
2536
|
-
};
|
|
2537
|
-
}, [enabled, followedId, workflowKey, pollTick]);
|
|
2538
|
-
const createCase = useCallback(
|
|
2539
|
-
async (input) => {
|
|
2540
|
-
const result = await getFluidGridsClient().mutate({
|
|
2541
|
-
mutation: CreateWorkflowTestCaseDocument,
|
|
2542
|
-
variables: { workflowKey, input }
|
|
2543
|
-
});
|
|
2544
|
-
throwIfResultError(result);
|
|
2545
|
-
const created = result.data?.createWorkflowTestCase;
|
|
2546
|
-
if (!created) throw new Error("The test case was not saved.");
|
|
2547
|
-
await refetch();
|
|
2548
|
-
return created;
|
|
2549
|
-
},
|
|
2550
|
-
[workflowKey, refetch]
|
|
2551
|
-
);
|
|
2552
|
-
const updateCase = useCallback(
|
|
2553
|
-
async (id, input) => {
|
|
2554
|
-
const result = await getFluidGridsClient().mutate({
|
|
2555
|
-
mutation: UpdateWorkflowTestCaseDocument,
|
|
2556
|
-
variables: { id, input }
|
|
2557
|
-
});
|
|
2558
|
-
throwIfResultError(result);
|
|
2559
|
-
const updated = result.data?.updateWorkflowTestCase;
|
|
2560
|
-
if (!updated) throw new Error("The test case was not saved.");
|
|
2561
|
-
await refetch();
|
|
2562
|
-
return updated;
|
|
2563
|
-
},
|
|
2564
|
-
[refetch]
|
|
2565
|
-
);
|
|
2566
|
-
const deleteCase = useCallback(
|
|
2567
|
-
async (id) => {
|
|
2568
|
-
const result = await getFluidGridsClient().mutate({
|
|
2569
|
-
mutation: DeleteWorkflowTestCaseDocument,
|
|
2570
|
-
variables: { id }
|
|
2571
|
-
});
|
|
2572
|
-
throwIfResultError(result);
|
|
2573
|
-
await refetch();
|
|
2574
|
-
},
|
|
2575
|
-
[refetch]
|
|
2576
|
-
);
|
|
2577
|
-
const runTests = useCallback(
|
|
2578
|
-
async (options = {}) => {
|
|
2579
|
-
const result = await getFluidGridsClient().mutate({
|
|
2580
|
-
mutation: RunWorkflowTestsDocument,
|
|
2581
|
-
variables: {
|
|
2582
|
-
input: {
|
|
2583
|
-
workflowKey,
|
|
2584
|
-
source: "UI" /* Ui */,
|
|
2585
|
-
...options.version ? { version: options.version } : {},
|
|
2586
|
-
...options.caseIds ? { caseIds: options.caseIds } : {}
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
});
|
|
2590
|
-
throwIfResultError(result);
|
|
2591
|
-
const run = result.data?.runWorkflowTests;
|
|
2592
|
-
if (!run) throw new Error("The test run did not start.");
|
|
2593
|
-
setRuns((current) => upsertRun(current, run));
|
|
2594
|
-
setActiveRunId(run.id);
|
|
2595
|
-
return run;
|
|
2596
|
-
},
|
|
2597
|
-
[workflowKey]
|
|
2598
|
-
);
|
|
2599
|
-
return {
|
|
2600
|
-
cases,
|
|
2601
|
-
runs,
|
|
2602
|
-
activeRun,
|
|
2603
|
-
loading,
|
|
2604
|
-
error,
|
|
2605
|
-
forbidden,
|
|
2606
|
-
refetch,
|
|
2607
|
-
createCase,
|
|
2608
|
-
updateCase,
|
|
2609
|
-
deleteCase,
|
|
2610
|
-
runTests
|
|
2611
|
-
};
|
|
2612
|
-
}
|
|
2613
|
-
async function saveRunAsTestCase(runId, name, description) {
|
|
2614
|
-
const result = await getFluidGridsClient().mutate({
|
|
2615
|
-
mutation: CreateWorkflowTestCaseFromRunDocument,
|
|
2616
|
-
variables: {
|
|
2617
|
-
runId,
|
|
2618
|
-
name,
|
|
2619
|
-
...description ? { description } : {}
|
|
2620
|
-
}
|
|
2621
|
-
});
|
|
2622
|
-
throwIfResultError(result);
|
|
2623
|
-
const created = result.data?.createWorkflowTestCaseFromRun;
|
|
2624
|
-
if (!created) throw new Error("The test case was not saved.");
|
|
2625
|
-
return created;
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
export { ASSERT_OPERATORS, EntityTagPicker, MAX_CASE_NAME, MAX_TEST_CASES, RunHistoryView, SignalRunDialog, TEST_STATUS_TONE_CLASS, WorkflowProposalCard, canSaveRunAsTestCase, describeAssertRuleProblem, draftFromTestCase, draftToInput, emptyAssertRuleRow, emptyTestCaseDraft, enabledCaseIds, isTestRunOpen, nextSortOrder, normaliseAssertRules, operatorSpec, pinnedOutputsFromCanvas, readAssertRulesSetting, rowsFromRules, ruleFromRow, saveRunAsTestCase, terminateDurableRun, testRunSummaryText, testStatusLabel, testStatusTone, useDeterminismStatus, useDurableRunFields, useRunHistory, useRunStackTrace, useWorkflowProposal, useWorkflowTests };
|