@burdenoff/fluidgrids-fe-sdk 2026.1107.1 → 2026.1108.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.
Files changed (64) hide show
  1. package/dist/{ExecutionDetailPage-TQST7JKC.js → ExecutionDetailPage-PY5OE2RJ.js} +16 -15
  2. package/dist/{ExecutionsListPage-QQKEGXSC.js → ExecutionsListPage-LKNYPXNZ.js} +12 -11
  3. package/dist/{MissingNodesInstallModal-OSAJLIBE.js → MissingNodesInstallModal-Q6OPULJU.js} +2 -1
  4. package/dist/{TemplateConfigurationModal-SILENYXX.js → TemplateConfigurationModal-PWCDDPKT.js} +2 -1
  5. package/dist/{TemplateDetailPanel-7MPY5MYT.js → TemplateDetailPanel-5WY75DVB.js} +3 -2
  6. package/dist/WorkflowBuilder-YFPBMOWZ.js +37 -0
  7. package/dist/{WorkflowsList-2WX7N526.js → WorkflowsList-TETNEMYP.js} +11 -11
  8. package/dist/{agentTools-MEEGOAE2.js → agentTools-BW2XMHRV.js} +7 -7
  9. package/dist/{chunk-KE677WYY.js → chunk-2CXUTQUH.js} +1 -1
  10. package/dist/{chunk-H4ERTSMX.js → chunk-2VSYOGNI.js} +1948 -35
  11. package/dist/{chunk-NZMUKAXG.js → chunk-54NXUOHI.js} +1 -1
  12. package/dist/{chunk-E3PNEIBJ.js → chunk-62RJORF7.js} +5 -5
  13. package/dist/{chunk-4F54UT62.js → chunk-66AKFXP7.js} +21 -18
  14. package/dist/{chunk-MESGVHUU.js → chunk-7SSMRID2.js} +5 -5
  15. package/dist/{chunk-752BT4WM.js → chunk-7XW6BC4B.js} +1 -1
  16. package/dist/{chunk-5375NUIO.js → chunk-AR22A74Y.js} +11 -0
  17. package/dist/{chunk-5KK3ALQ4.js → chunk-D5Y3DHGJ.js} +1 -1
  18. package/dist/{chunk-NSDBXKIC.js → chunk-DG4UO5OY.js} +5 -5
  19. package/dist/{chunk-NMYENCP2.js → chunk-E7KIH3CF.js} +1 -1
  20. package/dist/{chunk-HUCRE45R.js → chunk-GI6BQIQA.js} +127 -42
  21. package/dist/{chunk-Z3ABLERJ.js → chunk-GOZR72NK.js} +64 -1
  22. package/dist/{chunk-KHS2DZHH.js → chunk-HICQ4J4W.js} +1 -1
  23. package/dist/chunk-HPNZ3PYU.js +1448 -0
  24. package/dist/{chunk-RI4NFNDX.js → chunk-HQSSW7K3.js} +8 -8
  25. package/dist/{chunk-CTKWMPQQ.js → chunk-HSOSMPRC.js} +3 -1
  26. package/dist/{chunk-JRSCUUBT.js → chunk-IPJV2IOH.js} +1 -1
  27. package/dist/{chunk-PYSEOWLW.js → chunk-IWLNBDR2.js} +5 -5
  28. package/dist/{chunk-MYAFMXFI.js → chunk-IZOPKR64.js} +3 -3
  29. package/dist/{chunk-ZWDKN6PU.js → chunk-JRR4VKYD.js} +95 -6
  30. package/dist/{chunk-TDFC5U66.js → chunk-JSJSBB76.js} +2 -2
  31. package/dist/{chunk-V2K4HJY4.js → chunk-KJB3WZVD.js} +2 -2
  32. package/dist/{chunk-OHKSDHNC.js → chunk-L7PHZ7RY.js} +2 -2
  33. package/dist/{chunk-JOOGRWYY.js → chunk-LOMLLWHD.js} +705 -12
  34. package/dist/{chunk-ZTZNQ5GF.js → chunk-MYPB2KTJ.js} +13 -6
  35. package/dist/{chunk-HY3XCZLB.js → chunk-N2QXEV5Y.js} +8 -8
  36. package/dist/{chunk-EXJW7EKR.js → chunk-OJHBCOBA.js} +164 -1
  37. package/dist/{chunk-BLLBUHVF.js → chunk-POYMF7L6.js} +1 -1
  38. package/dist/{chunk-BQXVT32O.js → chunk-QJAZXCFQ.js} +1 -23
  39. package/dist/{chunk-GA3V3X7Q.js → chunk-RNYFPG7R.js} +4 -4
  40. package/dist/chunk-TFCIZFDM.js +28 -0
  41. package/dist/{chunk-PASKZCII.js → chunk-TN3NGTF7.js} +3 -3
  42. package/dist/{chunk-KXCJPWYE.js → chunk-VH3EET6H.js} +2 -2
  43. package/dist/{chunk-UZJKX5FN.js → chunk-X3F5IQBP.js} +5 -3
  44. package/dist/chunk-XRGAH6IQ.js +110 -0
  45. package/dist/chunk-YASVILWQ.js +398 -0
  46. package/dist/{chunk-WBVBRXDI.js → chunk-ZHXJYOOG.js} +2115 -25
  47. package/dist/executions.js +17 -16
  48. package/dist/history.js +9 -9
  49. package/dist/index.d.ts +15474 -238
  50. package/dist/index.js +1064 -58
  51. package/dist/{node-detail-view-AQCGO4XQ.js → node-detail-view-BGWKE6KW.js} +490 -22
  52. package/dist/nodes.js +5 -2
  53. package/dist/observability.js +9 -9
  54. package/dist/{quick-config-panel-G3T4JCF6.js → quick-config-panel-67BU4I23.js} +9 -9
  55. package/dist/settings.js +11 -9
  56. package/dist/templates.js +19 -17
  57. package/dist/{workflow-builder-C261exmL.d.ts → workflow-builder-D7eMRluc.d.ts} +3 -1
  58. package/dist/workflow-builder.d.ts +1 -1
  59. package/dist/workflow-builder.js +21 -19
  60. package/dist/workflow-preview.js +2 -2
  61. package/dist/workflows-list.js +11 -11
  62. package/package.json +2 -2
  63. package/dist/WorkflowBuilder-R7VIOVRO.js +0 -35
  64. package/dist/chunk-VKC3ICRA.js +0 -529
@@ -0,0 +1,1448 @@
1
+ import { useRunStatusCounts, ExecutionStats, ExecutionFilters, ExecutionTable, ExecutionCards, ExecutionTimeline, Pagination, runStatusLabels } from './chunk-RNYFPG7R.js';
2
+ import { useExecutionsWithStore, useExecutionStats, useExecutionActions, useAutoRefresh, searchFilterConflicts, WORKFLOW_ID_MAX_LENGTH, EMPTY_SEARCH_FILTER, buildSearchAttributeFilter, MAX_SEARCH_CONDITIONS, OPERATORS_BY_TYPE } from './chunk-JRR4VKYD.js';
3
+ import { useResource, fetchSearchAttributes, NativeSelect, FieldError, RUN_CONTROL_DIALOG_STYLE, parseJsonInput, JsonField, fetchBatchOperations, StatusPill, fetchBatchOperation, cancelBatchOperation, startBatchTerminate, startBatchExport, startBatchOperation, jsonArg } from './chunk-YASVILWQ.js';
4
+ import { useWorkflowDefinitions } from './chunk-KJB3WZVD.js';
5
+ import { FluidGridsDocsLink } from './chunk-4VJ3MDOV.js';
6
+ import { useDurableParityEnabled, usePhaseCFlags } from './chunk-GI6BQIQA.js';
7
+ import { useFluidGridsPermission, FLUIDGRIDS_PERMISSIONS } from './chunk-AR22A74Y.js';
8
+ import { WorkflowBatchOperationUpdatedDocument } from './chunk-GOZR72NK.js';
9
+ import { useTr } from './chunk-UULVIT25.js';
10
+ import { getFluidGridsClient } from './chunk-PDODDDHY.js';
11
+ import { describeOperationError } from './chunk-JKLZ6YZ3.js';
12
+ import { useExecutionsStore } from './chunk-UEG2WC3G.js';
13
+ import { useRef, useState, useEffect, useMemo, useCallback } from 'react';
14
+ import { useNavigate, useSearchParams } from 'react-router';
15
+ import { Icon } from '@iconify/react';
16
+ import { toast } from 'sonner';
17
+ import { Button } from '@burdenoff/fe-libs/button';
18
+ import { Input, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Label, DialogFooter, Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@burdenoff/fe-libs/ui';
19
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
20
+ import { cn } from '@burdenoff/fe-libs/utils';
21
+
22
+ var OPERATOR_LABELS = {
23
+ EQ: "=",
24
+ NEQ: "\u2260",
25
+ IN: "in",
26
+ GT: ">",
27
+ GTE: "\u2265",
28
+ LT: "<",
29
+ LTE: "\u2264",
30
+ STARTS_WITH: "starts with",
31
+ CONTAINS: "contains"
32
+ };
33
+ function SearchAttributeFilterBar({
34
+ applied,
35
+ onApply,
36
+ conflicts
37
+ }) {
38
+ const tr = useTr();
39
+ const definitions = useResource(
40
+ "search-attributes",
41
+ true,
42
+ fetchSearchAttributes
43
+ );
44
+ const [state, setState] = useState(EMPTY_SEARCH_FILTER);
45
+ const types = useMemo(
46
+ () => new Map(
47
+ (definitions.data ?? []).map((definition) => [
48
+ definition.name,
49
+ definition.type
50
+ ])
51
+ ),
52
+ [definitions.data]
53
+ );
54
+ const built = buildSearchAttributeFilter(state, types);
55
+ const names = [...types.keys()].sort();
56
+ const setRow = (index, patch) => setState((current) => ({
57
+ ...current,
58
+ rows: current.rows.map((row, i) => {
59
+ if (i !== index) return row;
60
+ const next = { ...row, ...patch };
61
+ const type = types.get(next.name);
62
+ if (type && !OPERATORS_BY_TYPE[type].includes(next.op))
63
+ next.op = OPERATORS_BY_TYPE[type][0];
64
+ return next;
65
+ })
66
+ }));
67
+ const addRow = () => {
68
+ const name = names[0];
69
+ if (!name) return;
70
+ const type = types.get(name);
71
+ setState((current) => ({
72
+ ...current,
73
+ rows: [
74
+ ...current.rows,
75
+ { name, op: type ? OPERATORS_BY_TYPE[type][0] : "EQ", value: "" }
76
+ ]
77
+ }));
78
+ };
79
+ return /* @__PURE__ */ jsxs(
80
+ "section",
81
+ {
82
+ className: "mb-4 space-y-3 rounded-lg border border-border-secondary bg-background-secondary p-3",
83
+ "data-testid": "search-attribute-filter",
84
+ children: [
85
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
86
+ /* @__PURE__ */ jsx(
87
+ Icon,
88
+ {
89
+ icon: "mdi:tag-search-outline",
90
+ width: 18,
91
+ className: "text-text-secondary"
92
+ }
93
+ ),
94
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-text-primary", children: tr("fluidgrids.runControl.searchFilter.title", "Search attributes") }),
95
+ state.rows.length > 1 && /* @__PURE__ */ jsxs(
96
+ NativeSelect,
97
+ {
98
+ value: state.combine,
99
+ onChange: (value) => setState((current) => ({
100
+ ...current,
101
+ combine: value === "or" ? "or" : "and"
102
+ })),
103
+ ariaLabel: tr(
104
+ "fluidgrids.runControl.searchFilter.combine",
105
+ "Match"
106
+ ),
107
+ testId: "search-filter-combine",
108
+ children: [
109
+ /* @__PURE__ */ jsx("option", { value: "and", children: tr("fluidgrids.runControl.searchFilter.all", "all conditions") }),
110
+ /* @__PURE__ */ jsx("option", { value: "or", children: tr("fluidgrids.runControl.searchFilter.any", "any condition") })
111
+ ]
112
+ }
113
+ ),
114
+ /* @__PURE__ */ jsx(
115
+ Input,
116
+ {
117
+ value: state.text,
118
+ onChange: (event) => setState((current) => ({ ...current, text: event.target.value })),
119
+ placeholder: tr(
120
+ "fluidgrids.runControl.searchFilter.text",
121
+ "Words in text attributes"
122
+ ),
123
+ className: "h-9 max-w-xs",
124
+ maxLength: 256,
125
+ "data-testid": "search-filter-text"
126
+ }
127
+ ),
128
+ /* @__PURE__ */ jsxs(
129
+ Button,
130
+ {
131
+ size: "sm",
132
+ variant: "outline",
133
+ onClick: addRow,
134
+ disabled: names.length === 0 || state.rows.length >= MAX_SEARCH_CONDITIONS,
135
+ "data-testid": "search-filter-add",
136
+ children: [
137
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:plus", className: "mr-1", width: 14 }),
138
+ tr("fluidgrids.runControl.searchFilter.add", "Condition")
139
+ ]
140
+ }
141
+ ),
142
+ /* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-2", children: [
143
+ applied && /* @__PURE__ */ jsx(
144
+ Button,
145
+ {
146
+ size: "sm",
147
+ variant: "ghost",
148
+ onClick: () => {
149
+ setState(EMPTY_SEARCH_FILTER);
150
+ onApply(null);
151
+ },
152
+ "data-testid": "search-filter-clear",
153
+ children: tr("fluidgrids.runControl.searchFilter.clear", "Clear")
154
+ }
155
+ ),
156
+ /* @__PURE__ */ jsx(
157
+ Button,
158
+ {
159
+ size: "sm",
160
+ onClick: () => onApply(built.filter),
161
+ disabled: built.incomplete > 0,
162
+ "data-testid": "search-filter-apply",
163
+ children: tr("fluidgrids.runControl.searchFilter.apply", "Apply")
164
+ }
165
+ )
166
+ ] })
167
+ ] }),
168
+ definitions.data && names.length === 0 && /* @__PURE__ */ jsx(
169
+ "p",
170
+ {
171
+ className: "text-xs text-text-secondary",
172
+ "data-testid": "search-filter-no-definitions",
173
+ children: tr(
174
+ "fluidgrids.runControl.searchFilter.noDefinitions",
175
+ "No search attributes are defined. Define them in Settings \u2192 Run control."
176
+ )
177
+ }
178
+ ),
179
+ /* @__PURE__ */ jsx(FieldError, { message: definitions.error }),
180
+ state.rows.map((row, index) => {
181
+ const type = types.get(row.name);
182
+ return /* @__PURE__ */ jsxs(
183
+ "div",
184
+ {
185
+ className: "flex flex-wrap items-center gap-2",
186
+ "data-testid": `search-filter-row-${index}`,
187
+ children: [
188
+ /* @__PURE__ */ jsx("div", { className: "w-44", children: /* @__PURE__ */ jsx(
189
+ NativeSelect,
190
+ {
191
+ value: row.name,
192
+ onChange: (name) => setRow(index, { name }),
193
+ ariaLabel: tr(
194
+ "fluidgrids.runControl.searchFilter.attribute",
195
+ "Attribute"
196
+ ),
197
+ testId: `search-filter-name-${index}`,
198
+ children: names.map((name) => /* @__PURE__ */ jsx("option", { value: name, children: name }, name))
199
+ }
200
+ ) }),
201
+ /* @__PURE__ */ jsx("div", { className: "w-32", children: /* @__PURE__ */ jsx(
202
+ NativeSelect,
203
+ {
204
+ value: row.op,
205
+ onChange: (op) => setRow(index, {
206
+ op: (type ? OPERATORS_BY_TYPE[type] : []).find(
207
+ (item) => item === op
208
+ ) ?? row.op
209
+ }),
210
+ ariaLabel: tr(
211
+ "fluidgrids.runControl.searchFilter.operator",
212
+ "Operator"
213
+ ),
214
+ testId: `search-filter-op-${index}`,
215
+ children: (type ? OPERATORS_BY_TYPE[type] : []).map((op) => /* @__PURE__ */ jsx("option", { value: op, children: OPERATOR_LABELS[op] }, op))
216
+ }
217
+ ) }),
218
+ /* @__PURE__ */ jsx(
219
+ Input,
220
+ {
221
+ value: row.value,
222
+ onChange: (event) => setRow(index, { value: event.target.value }),
223
+ placeholder: row.op === "IN" ? tr(
224
+ "fluidgrids.runControl.searchFilter.listValue",
225
+ "a, b, c"
226
+ ) : type ?? "",
227
+ className: "h-9 max-w-xs",
228
+ "aria-label": tr(
229
+ "fluidgrids.runControl.searchFilter.value",
230
+ "Value"
231
+ ),
232
+ "data-testid": `search-filter-value-${index}`
233
+ }
234
+ ),
235
+ /* @__PURE__ */ jsx(
236
+ Button,
237
+ {
238
+ size: "sm",
239
+ variant: "ghost",
240
+ "aria-label": tr(
241
+ "fluidgrids.runControl.searchFilter.remove",
242
+ "Remove condition"
243
+ ),
244
+ onClick: () => setState((current) => ({
245
+ ...current,
246
+ rows: current.rows.filter((_, i) => i !== index)
247
+ })),
248
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mdi:close", width: 14 })
249
+ }
250
+ )
251
+ ]
252
+ },
253
+ index
254
+ );
255
+ }),
256
+ built.incomplete > 0 && /* @__PURE__ */ jsx(
257
+ "p",
258
+ {
259
+ className: "text-xs text-[var(--color-status-warning-text)]",
260
+ "data-testid": "search-filter-incomplete",
261
+ children: tr(
262
+ "fluidgrids.runControl.searchFilter.incomplete",
263
+ "Give every condition a value of its attribute's type."
264
+ )
265
+ }
266
+ ),
267
+ applied && conflicts.length > 0 && /* @__PURE__ */ jsx(
268
+ "p",
269
+ {
270
+ className: "text-xs text-[var(--color-status-warning-text)]",
271
+ "data-testid": "search-filter-conflicts",
272
+ children: tr(
273
+ "fluidgrids.runControl.searchFilter.conflicts",
274
+ "The attribute filter is not applied while these filters are set: {{filters}}. Clear them to use it.",
275
+ { filters: conflicts.join(", ") }
276
+ )
277
+ }
278
+ )
279
+ ]
280
+ }
281
+ );
282
+ }
283
+
284
+ // src/modules/run-control/utils/batch.ts
285
+ var BATCH_MENU_OPERATIONS = [
286
+ "SIGNAL",
287
+ "CANCEL",
288
+ "TERMINATE",
289
+ "RESET",
290
+ "PAUSE_ACTIVITY",
291
+ "UNPAUSE_ACTIVITY",
292
+ "RESET_ACTIVITY",
293
+ "EXPORT"
294
+ ];
295
+ var DURABLE_BATCH_OPERATIONS = /* @__PURE__ */ new Set([
296
+ "RESET",
297
+ "PAUSE_ACTIVITY",
298
+ "UNPAUSE_ACTIVITY",
299
+ "RESET_ACTIVITY"
300
+ ]);
301
+ function allowedBatchOperations(gates, permissions) {
302
+ const allowed = /* @__PURE__ */ new Set();
303
+ if (!gates.runControl) return allowed;
304
+ if (permissions.canExecute)
305
+ for (const type of BATCH_MENU_OPERATIONS) {
306
+ if (type === "TERMINATE" || type === "EXPORT") continue;
307
+ if (DURABLE_BATCH_OPERATIONS.has(type) && !gates.runControlDurable)
308
+ continue;
309
+ allowed.add(type);
310
+ }
311
+ if (permissions.canDelete) allowed.add("TERMINATE");
312
+ if (gates.runArchival) allowed.add("EXPORT");
313
+ return allowed;
314
+ }
315
+ function batchMutationOf(type) {
316
+ if (type === "TERMINATE") return "terminate";
317
+ if (type === "EXPORT") return "export";
318
+ return "operation";
319
+ }
320
+ var MAX_BATCH_RUN_IDS = 1e3;
321
+ function batchSelection(input, selection) {
322
+ if (selection.allMatching) {
323
+ const filter = { ...input };
324
+ delete filter.limit;
325
+ delete filter.offset;
326
+ return { filter };
327
+ }
328
+ return { runIds: selection.runIds.slice(0, MAX_BATCH_RUN_IDS) };
329
+ }
330
+ function needsActivitySelector(type) {
331
+ return type === "PAUSE_ACTIVITY" || type === "UNPAUSE_ACTIVITY" || type === "RESET_ACTIVITY";
332
+ }
333
+ function batchProgress(job) {
334
+ const done = job.succeeded + job.failed + job.skipped;
335
+ const total = Math.max(job.total, 0);
336
+ const finished = job.status === "COMPLETED" || job.status === "FAILED" || job.status === "CANCELLED";
337
+ const percent = total === 0 ? finished ? 100 : 0 : Math.min(100, Math.round(done / total * 100));
338
+ return { done, total, percent, finished };
339
+ }
340
+ var BATCH_POLL_INTERVAL_MS = 3e3;
341
+ var OPERATION_TYPES = {
342
+ SIGNAL: "SIGNAL" /* Signal */,
343
+ CANCEL: "CANCEL" /* Cancel */,
344
+ TERMINATE: "TERMINATE" /* Terminate */,
345
+ RESET: "RESET" /* Reset */,
346
+ PAUSE_ACTIVITY: "PAUSE_ACTIVITY" /* PauseActivity */,
347
+ UNPAUSE_ACTIVITY: "UNPAUSE_ACTIVITY" /* UnpauseActivity */,
348
+ RESET_ACTIVITY: "RESET_ACTIVITY" /* ResetActivity */,
349
+ EXPORT: "EXPORT" /* Export */
350
+ };
351
+ function useBatchOperationLabels() {
352
+ const tr = useTr();
353
+ return {
354
+ SIGNAL: tr("fluidgrids.runControl.batch.op.signal", "Signal"),
355
+ CANCEL: tr("fluidgrids.runControl.batch.op.cancel", "Cancel"),
356
+ TERMINATE: tr("fluidgrids.runControl.batch.op.terminate", "Terminate"),
357
+ RESET: tr(
358
+ "fluidgrids.runControl.batch.op.reset",
359
+ "Reset to last clean task"
360
+ ),
361
+ PAUSE_ACTIVITY: tr(
362
+ "fluidgrids.runControl.batch.op.pauseActivity",
363
+ "Pause activities"
364
+ ),
365
+ UNPAUSE_ACTIVITY: tr(
366
+ "fluidgrids.runControl.batch.op.unpauseActivity",
367
+ "Unpause activities"
368
+ ),
369
+ RESET_ACTIVITY: tr(
370
+ "fluidgrids.runControl.batch.op.resetActivity",
371
+ "Reset activity attempts"
372
+ ),
373
+ EXPORT: tr("fluidgrids.runControl.batch.op.export", "Export to archive")
374
+ };
375
+ }
376
+ function BatchMenu({ allowed, onPick, onShowJobs }) {
377
+ const tr = useTr();
378
+ const labels = useBatchOperationLabels();
379
+ const offered = BATCH_MENU_OPERATIONS.filter((type) => allowed.has(type));
380
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
381
+ /* @__PURE__ */ jsxs(DropdownMenu, { children: [
382
+ /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
383
+ Button,
384
+ {
385
+ variant: "outline",
386
+ size: "sm",
387
+ disabled: offered.length === 0,
388
+ "data-testid": "batch-menu",
389
+ children: [
390
+ /* @__PURE__ */ jsx(
391
+ Icon,
392
+ {
393
+ icon: "mdi:layers-triple-outline",
394
+ className: "mr-1",
395
+ width: 14
396
+ }
397
+ ),
398
+ tr("fluidgrids.runControl.batch.menu", "Batch"),
399
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:chevron-down", className: "ml-1", width: 14 })
400
+ ]
401
+ }
402
+ ) }),
403
+ /* @__PURE__ */ jsx(DropdownMenuContent, { style: RUN_CONTROL_DIALOG_STYLE, children: offered.map((type) => /* @__PURE__ */ jsx(
404
+ DropdownMenuItem,
405
+ {
406
+ onSelect: () => onPick(type),
407
+ "data-testid": `batch-op-${type}`,
408
+ children: labels[type]
409
+ },
410
+ type
411
+ )) })
412
+ ] }),
413
+ /* @__PURE__ */ jsx(
414
+ Button,
415
+ {
416
+ variant: "ghost",
417
+ size: "sm",
418
+ onClick: onShowJobs,
419
+ "data-testid": "batch-jobs-button",
420
+ children: tr("fluidgrids.runControl.batch.jobs", "Batch jobs")
421
+ }
422
+ )
423
+ ] });
424
+ }
425
+ async function startJob(type, selection, common, payload) {
426
+ const mutation = batchMutationOf(type);
427
+ if (mutation === "terminate")
428
+ return startBatchTerminate({ ...selection, ...common });
429
+ if (mutation === "export")
430
+ return startBatchExport({ ...selection, ...common });
431
+ return startBatchOperation({
432
+ ...selection,
433
+ ...common,
434
+ type: OPERATION_TYPES[type],
435
+ payload
436
+ });
437
+ }
438
+ function BatchOperationDialog({
439
+ type,
440
+ input,
441
+ selection,
442
+ onClose,
443
+ onStarted
444
+ }) {
445
+ const tr = useTr();
446
+ const labels = useBatchOperationLabels();
447
+ const [reason, setReason] = useState("");
448
+ const [rate, setRate] = useState("50");
449
+ const [signalName, setSignalName] = useState("");
450
+ const [signalPayload, setSignalPayload] = useState("");
451
+ const [pauseReason, setPauseReason] = useState("");
452
+ const [dryRun, setDryRun] = useState(null);
453
+ const [busy, setBusy] = useState(null);
454
+ const [error, setError] = useState(null);
455
+ const parsedPayload = parseJsonInput(signalPayload);
456
+ const maxOperationsPerSecond = Math.min(
457
+ 50,
458
+ Math.max(1, Math.round(Number(rate) || 0))
459
+ );
460
+ const payload = () => {
461
+ if (type === "SIGNAL")
462
+ return {
463
+ signalName: signalName.trim(),
464
+ signalPayload: parsedPayload.ok ? jsonArg(parsedPayload.value) : null
465
+ };
466
+ if (type === "RESET")
467
+ return { resetTo: { type: "LAST_TASK" /* LastTask */ } };
468
+ if (needsActivitySelector(type)) return { selector: { all: true } };
469
+ return null;
470
+ };
471
+ const valid = reason.trim().length > 0 && Number.isFinite(Number(rate)) && (type !== "SIGNAL" || signalName.trim().length > 0 && parsedPayload.ok) && (type !== "PAUSE_ACTIVITY" || pauseReason.trim().length > 0);
472
+ const jobReason = type === "PAUSE_ACTIVITY" && pauseReason.trim() ? pauseReason.trim() : reason.trim();
473
+ const run = async (asDryRun) => {
474
+ if (!valid) return;
475
+ setBusy(asDryRun ? "dry" : "start");
476
+ setError(null);
477
+ try {
478
+ const chosen = batchSelection(input, selection);
479
+ const target = "filter" in chosen ? { filter: chosen.filter } : { runIds: chosen.runIds };
480
+ const job = await startJob(
481
+ type,
482
+ target,
483
+ { reason: jobReason, maxOperationsPerSecond, dryRun: asDryRun },
484
+ payload()
485
+ );
486
+ if (asDryRun) setDryRun(job);
487
+ else onStarted(job);
488
+ } catch (err) {
489
+ setError(describeOperationError(err));
490
+ } finally {
491
+ setBusy(null);
492
+ }
493
+ };
494
+ return /* @__PURE__ */ jsx(Dialog, { open: true, onOpenChange: (open) => !open && onClose(), children: /* @__PURE__ */ jsxs(
495
+ DialogContent,
496
+ {
497
+ style: RUN_CONTROL_DIALOG_STYLE,
498
+ "data-testid": "batch-dialog",
499
+ children: [
500
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
501
+ /* @__PURE__ */ jsx(DialogTitle, { children: labels[type] }),
502
+ /* @__PURE__ */ jsx(DialogDescription, { children: selection.allMatching ? tr(
503
+ "fluidgrids.runControl.batch.allMatching",
504
+ "Every run that matches the list's filters."
505
+ ) : tr(
506
+ "fluidgrids.runControl.batch.selected",
507
+ "{{count}} selected runs.",
508
+ { count: Math.min(selection.runIds.length, 1e3) }
509
+ ) })
510
+ ] }),
511
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
512
+ type === "SIGNAL" && /* @__PURE__ */ jsxs(Fragment, { children: [
513
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
514
+ /* @__PURE__ */ jsx(Label, { htmlFor: "batch-signal-name", children: tr("fluidgrids.runControl.batch.signalName", "Signal name") }),
515
+ /* @__PURE__ */ jsx(
516
+ Input,
517
+ {
518
+ id: "batch-signal-name",
519
+ value: signalName,
520
+ onChange: (event) => setSignalName(event.target.value),
521
+ "data-testid": "batch-signal-name"
522
+ }
523
+ )
524
+ ] }),
525
+ /* @__PURE__ */ jsx(
526
+ JsonField,
527
+ {
528
+ id: "batch-signal-payload",
529
+ label: tr(
530
+ "fluidgrids.runControl.batch.signalPayload",
531
+ "Payload (JSON, optional)"
532
+ ),
533
+ value: signalPayload,
534
+ onChange: setSignalPayload,
535
+ rows: 3,
536
+ error: parsedPayload.ok ? null : parsedPayload.error
537
+ }
538
+ )
539
+ ] }),
540
+ type === "PAUSE_ACTIVITY" && /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
541
+ /* @__PURE__ */ jsx(Label, { htmlFor: "batch-pause-reason", children: tr("fluidgrids.runControl.batch.pauseReason", "Pause reason") }),
542
+ /* @__PURE__ */ jsx(
543
+ Input,
544
+ {
545
+ id: "batch-pause-reason",
546
+ value: pauseReason,
547
+ onChange: (event) => setPauseReason(event.target.value),
548
+ "data-testid": "batch-pause-reason"
549
+ }
550
+ )
551
+ ] }),
552
+ needsActivitySelector(type) && /* @__PURE__ */ jsx("p", { className: "text-xs text-text-secondary", children: tr(
553
+ "fluidgrids.runControl.batch.allActivities",
554
+ "Applies to every open activity of each run."
555
+ ) }),
556
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
557
+ /* @__PURE__ */ jsx(Label, { htmlFor: "batch-reason", children: tr("fluidgrids.runControl.batch.reason", "Reason") }),
558
+ /* @__PURE__ */ jsx(
559
+ Input,
560
+ {
561
+ id: "batch-reason",
562
+ value: reason,
563
+ maxLength: 1e3,
564
+ onChange: (event) => setReason(event.target.value),
565
+ "data-testid": "batch-reason"
566
+ }
567
+ )
568
+ ] }),
569
+ /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
570
+ /* @__PURE__ */ jsx(Label, { htmlFor: "batch-rate", children: tr(
571
+ "fluidgrids.runControl.batch.rate",
572
+ "Runs per second (1 to 50)"
573
+ ) }),
574
+ /* @__PURE__ */ jsx(
575
+ Input,
576
+ {
577
+ id: "batch-rate",
578
+ inputMode: "numeric",
579
+ value: rate,
580
+ onChange: (event) => setRate(event.target.value),
581
+ "data-testid": "batch-rate"
582
+ }
583
+ )
584
+ ] }),
585
+ dryRun && /* @__PURE__ */ jsx(
586
+ "p",
587
+ {
588
+ className: "rounded-md bg-background-primary p-2 text-sm text-text-primary",
589
+ "data-testid": "batch-dry-run-result",
590
+ children: tr(
591
+ "fluidgrids.runControl.batch.dryRunResult",
592
+ "The job would select {{count}} runs.",
593
+ { count: dryRun.total }
594
+ )
595
+ }
596
+ ),
597
+ /* @__PURE__ */ jsx(FieldError, { message: error, testId: "batch-error" })
598
+ ] }),
599
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
600
+ /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onClose, disabled: busy !== null, children: tr("fluidgrids.runControl.common.cancel", "Cancel") }),
601
+ /* @__PURE__ */ jsx(
602
+ Button,
603
+ {
604
+ variant: "outline",
605
+ onClick: () => void run(true),
606
+ disabled: !valid || busy !== null,
607
+ "data-testid": "batch-dry-run",
608
+ children: busy === "dry" ? tr("fluidgrids.runControl.batch.counting", "Counting\u2026") : tr("fluidgrids.runControl.batch.dryRun", "Dry run")
609
+ }
610
+ ),
611
+ /* @__PURE__ */ jsx(
612
+ Button,
613
+ {
614
+ onClick: () => void run(false),
615
+ disabled: !valid || busy !== null,
616
+ "data-testid": "batch-start",
617
+ children: busy === "start" ? tr("fluidgrids.runControl.common.working", "Working\u2026") : tr("fluidgrids.runControl.batch.start", "Start job")
618
+ }
619
+ )
620
+ ] })
621
+ ]
622
+ }
623
+ ) });
624
+ }
625
+ function statusTone(status) {
626
+ if (status === "COMPLETED") return "success";
627
+ if (status === "FAILED") return "error";
628
+ if (status === "CANCELLED") return "neutral";
629
+ return "info";
630
+ }
631
+ function isBatchMenuOperation(type) {
632
+ return BATCH_MENU_OPERATIONS.some((item) => item === type);
633
+ }
634
+ function useBatchJob(initial) {
635
+ const [job, setJob] = useState(initial);
636
+ const id = job?.id ?? null;
637
+ const finished = job ? batchProgress(job).finished : true;
638
+ useEffect(() => {
639
+ if (!id || finished) return;
640
+ let cancelled = false;
641
+ const subscription = getFluidGridsClient().subscribe({
642
+ query: WorkflowBatchOperationUpdatedDocument,
643
+ variables: { id }
644
+ }).subscribe({
645
+ next: ({ data }) => {
646
+ if (!cancelled && data?.workflowBatchOperationUpdated)
647
+ setJob(data.workflowBatchOperationUpdated);
648
+ },
649
+ // The timer below keeps the drawer current without the stream.
650
+ error: () => void 0
651
+ });
652
+ const timer = setInterval(() => {
653
+ fetchBatchOperation(id).then((next) => {
654
+ if (!cancelled) setJob(next);
655
+ }).catch(() => void 0);
656
+ }, BATCH_POLL_INTERVAL_MS);
657
+ return () => {
658
+ cancelled = true;
659
+ subscription.unsubscribe();
660
+ clearInterval(timer);
661
+ };
662
+ }, [id, finished]);
663
+ return { job, setJob };
664
+ }
665
+ function BatchProgressDrawer({
666
+ job: initial,
667
+ open,
668
+ onOpenChange,
669
+ canCancel
670
+ }) {
671
+ const tr = useTr();
672
+ const labels = useBatchOperationLabels();
673
+ const { job, setJob } = useBatchJob(initial);
674
+ const [error, setError] = useState(null);
675
+ const jobs = useResource(
676
+ "batch-jobs",
677
+ open && !job,
678
+ () => fetchBatchOperations({ first: 20 })
679
+ );
680
+ useEffect(() => setJob(initial), [initial, setJob]);
681
+ const cancel = async () => {
682
+ if (!job) return;
683
+ try {
684
+ setJob(await cancelBatchOperation(job.id));
685
+ } catch (err) {
686
+ setError(describeOperationError(err));
687
+ }
688
+ };
689
+ const labelOf = (type) => isBatchMenuOperation(type) ? labels[type] : type;
690
+ return /* @__PURE__ */ jsx(Sheet, { open, onOpenChange, children: /* @__PURE__ */ jsxs(
691
+ SheetContent,
692
+ {
693
+ style: RUN_CONTROL_DIALOG_STYLE,
694
+ className: "w-full sm:max-w-md",
695
+ "data-testid": "batch-drawer",
696
+ children: [
697
+ /* @__PURE__ */ jsxs(SheetHeader, { children: [
698
+ /* @__PURE__ */ jsx(SheetTitle, { children: job ? labelOf(job.type) : tr("fluidgrids.runControl.batch.jobs", "Batch jobs") }),
699
+ /* @__PURE__ */ jsx(SheetDescription, { children: job ? job.reason : tr(
700
+ "fluidgrids.runControl.batch.jobsDescription",
701
+ "The workspace's recent batch jobs."
702
+ ) })
703
+ ] }),
704
+ job ? /* @__PURE__ */ jsx(
705
+ BatchJobProgress,
706
+ {
707
+ job,
708
+ onCancel: canCancel ? () => void cancel() : void 0
709
+ }
710
+ ) : /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-2", children: [
711
+ /* @__PURE__ */ jsx(FieldError, { message: jobs.error }),
712
+ (jobs.data?.items ?? []).map((item) => /* @__PURE__ */ jsxs(
713
+ "button",
714
+ {
715
+ type: "button",
716
+ className: "w-full rounded-md border border-border-secondary p-2 text-left hover:bg-background-tertiary",
717
+ onClick: () => setJob(item),
718
+ "data-testid": `batch-job-${item.id}`,
719
+ children: [
720
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-sm", children: [
721
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-text-primary", children: labelOf(item.type) }),
722
+ /* @__PURE__ */ jsx(StatusPill, { tone: statusTone(item.status), children: item.status }),
723
+ /* @__PURE__ */ jsxs("span", { className: "ml-auto text-xs text-text-secondary", children: [
724
+ batchProgress(item).percent,
725
+ "%"
726
+ ] })
727
+ ] }),
728
+ /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-text-secondary", children: item.reason })
729
+ ]
730
+ },
731
+ item.id
732
+ )),
733
+ jobs.data && jobs.data.items.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-text-secondary", children: tr("fluidgrids.runControl.batch.noJobs", "No batch jobs yet.") })
734
+ ] }),
735
+ /* @__PURE__ */ jsx(FieldError, { message: error })
736
+ ]
737
+ }
738
+ ) });
739
+ }
740
+ function BatchJobProgress({
741
+ job,
742
+ onCancel
743
+ }) {
744
+ const tr = useTr();
745
+ const progress = batchProgress(job);
746
+ return /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-3", "data-testid": "batch-progress", children: [
747
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
748
+ /* @__PURE__ */ jsx(
749
+ StatusPill,
750
+ {
751
+ tone: statusTone(job.status),
752
+ testId: "batch-progress-status",
753
+ children: job.status
754
+ }
755
+ ),
756
+ /* @__PURE__ */ jsxs(
757
+ "span",
758
+ {
759
+ className: "text-sm text-text-secondary",
760
+ "data-testid": "batch-progress-count",
761
+ children: [
762
+ progress.done,
763
+ " / ",
764
+ progress.total
765
+ ]
766
+ }
767
+ )
768
+ ] }),
769
+ /* @__PURE__ */ jsx("div", { className: "h-2 overflow-hidden rounded-full bg-background-tertiary", children: /* @__PURE__ */ jsx(
770
+ "div",
771
+ {
772
+ className: "h-full bg-[var(--color-status-info-bg)]",
773
+ style: { width: `${progress.percent}%` }
774
+ }
775
+ ) }),
776
+ /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-3 gap-2 text-center text-xs", children: [
777
+ /* @__PURE__ */ jsxs("div", { children: [
778
+ /* @__PURE__ */ jsx("dt", { className: "text-text-secondary", children: tr("fluidgrids.runControl.batch.succeeded", "Succeeded") }),
779
+ /* @__PURE__ */ jsx("dd", { className: "text-base font-semibold text-text-primary", children: job.succeeded })
780
+ ] }),
781
+ /* @__PURE__ */ jsxs("div", { children: [
782
+ /* @__PURE__ */ jsx("dt", { className: "text-text-secondary", children: tr("fluidgrids.runControl.batch.failed", "Failed") }),
783
+ /* @__PURE__ */ jsx("dd", { className: "text-base font-semibold text-text-primary", children: job.failed })
784
+ ] }),
785
+ /* @__PURE__ */ jsxs("div", { children: [
786
+ /* @__PURE__ */ jsx("dt", { className: "text-text-secondary", children: tr("fluidgrids.runControl.batch.skipped", "Skipped") }),
787
+ /* @__PURE__ */ jsx("dd", { className: "text-base font-semibold text-text-primary", children: job.skipped })
788
+ ] })
789
+ ] }),
790
+ job.failureMessage && /* @__PURE__ */ jsx(FieldError, { message: job.failureMessage }),
791
+ job.errors.length > 0 && /* @__PURE__ */ jsx(
792
+ "ul",
793
+ {
794
+ className: "max-h-48 space-y-1 overflow-auto text-xs",
795
+ "data-testid": "batch-progress-errors",
796
+ children: job.errors.map((item) => /* @__PURE__ */ jsxs("li", { children: [
797
+ /* @__PURE__ */ jsx("code", { className: "font-mono", children: item.runId.slice(0, 12) }),
798
+ " ",
799
+ item.message
800
+ ] }, item.runId))
801
+ }
802
+ ),
803
+ onCancel && !progress.finished && !job.cancelRequestedAt && /* @__PURE__ */ jsx(
804
+ Button,
805
+ {
806
+ variant: "outline",
807
+ size: "sm",
808
+ onClick: onCancel,
809
+ "data-testid": "batch-cancel",
810
+ children: tr("fluidgrids.runControl.batch.cancel", "Cancel job")
811
+ }
812
+ )
813
+ ] });
814
+ }
815
+ var WORKFLOW_ID_FILTER_DEBOUNCE_MS = 350;
816
+ var RUN_STATUSES = [
817
+ "PENDING",
818
+ "RUNNING",
819
+ "WAITING",
820
+ "COMPLETED",
821
+ "FAILED",
822
+ "CANCELLED",
823
+ "TERMINATED"
824
+ ];
825
+ function isRunStatus(value) {
826
+ return RUN_STATUSES.includes(value);
827
+ }
828
+ function RunsParityBar({
829
+ workflowId,
830
+ onWorkflowIdChange,
831
+ total,
832
+ counts,
833
+ countsError,
834
+ selectedStatus,
835
+ onSelectStatus
836
+ }) {
837
+ const tr = useTr();
838
+ const statusLabels = runStatusLabels(tr);
839
+ const [draft, setDraft] = useState(workflowId);
840
+ useEffect(() => {
841
+ setDraft(workflowId);
842
+ }, [workflowId]);
843
+ useEffect(() => {
844
+ if (draft === workflowId) return;
845
+ const timer = setTimeout(
846
+ () => onWorkflowIdChange(draft),
847
+ WORKFLOW_ID_FILTER_DEBOUNCE_MS
848
+ );
849
+ return () => clearTimeout(timer);
850
+ }, [draft, workflowId, onWorkflowIdChange]);
851
+ return /* @__PURE__ */ jsxs(
852
+ "div",
853
+ {
854
+ "data-testid": "runs-parity-bar",
855
+ className: "mb-6 flex flex-col gap-3 lg:flex-row lg:items-center",
856
+ children: [
857
+ /* @__PURE__ */ jsxs("label", { className: "flex min-w-0 items-center gap-2 text-sm text-text-secondary lg:w-80", children: [
858
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:key-outline", width: 16, className: "shrink-0" }),
859
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: tr("fluidgrids.executions.parity.workflowIdFilter", "Workflow ID") }),
860
+ /* @__PURE__ */ jsx(
861
+ Input,
862
+ {
863
+ value: draft,
864
+ maxLength: WORKFLOW_ID_MAX_LENGTH,
865
+ placeholder: tr(
866
+ "fluidgrids.executions.parity.workflowIdPlaceholder",
867
+ "Filter by workflow ID"
868
+ ),
869
+ onChange: (event) => setDraft(event.target.value),
870
+ "data-testid": "runs-workflow-id-filter"
871
+ }
872
+ )
873
+ ] }),
874
+ /* @__PURE__ */ jsx(
875
+ "div",
876
+ {
877
+ className: "flex flex-wrap items-center gap-2",
878
+ "data-testid": "runs-status-counts",
879
+ children: countsError ? /* @__PURE__ */ jsxs("span", { className: "text-xs text-[var(--color-status-error-text)]", children: [
880
+ tr(
881
+ "fluidgrids.executions.parity.countsError",
882
+ "Could not count the runs:"
883
+ ),
884
+ " ",
885
+ countsError
886
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
887
+ total !== null && /* @__PURE__ */ jsx(
888
+ "button",
889
+ {
890
+ type: "button",
891
+ onClick: () => onSelectStatus("all"),
892
+ className: cn(
893
+ "rounded-full border px-2.5 py-1 text-xs",
894
+ selectedStatus === "all" ? "border-action-primary-bg bg-action-primary-bg text-action-primary-text" : "border-border-secondary text-text-secondary hover:text-text-primary"
895
+ ),
896
+ "aria-pressed": selectedStatus === "all",
897
+ "data-testid": "runs-count-all",
898
+ children: tr("fluidgrids.executions.parity.countAll", "All {{count}}", {
899
+ count: total
900
+ })
901
+ }
902
+ ),
903
+ counts.map(
904
+ ({ status, count }) => isRunStatus(status) ? /* @__PURE__ */ jsx(
905
+ "button",
906
+ {
907
+ type: "button",
908
+ onClick: () => onSelectStatus(status),
909
+ className: cn(
910
+ "rounded-full border px-2.5 py-1 text-xs",
911
+ selectedStatus === status ? "border-action-primary-bg bg-action-primary-bg text-action-primary-text" : "border-border-secondary text-text-secondary hover:text-text-primary"
912
+ ),
913
+ "aria-pressed": selectedStatus === status,
914
+ "data-testid": `runs-count-${status}`,
915
+ children: tr(
916
+ "fluidgrids.executions.parity.countStatus",
917
+ "{{status}} {{count}}",
918
+ { status: statusLabels[status], count }
919
+ )
920
+ },
921
+ status
922
+ ) : null
923
+ )
924
+ ] })
925
+ }
926
+ )
927
+ ]
928
+ }
929
+ );
930
+ }
931
+ function ExecutionsListPage() {
932
+ const tr = useTr();
933
+ const navigate = useNavigate();
934
+ const {
935
+ viewMode,
936
+ setViewMode,
937
+ setSelectedExecution,
938
+ selection,
939
+ clearSelection,
940
+ setStatusFilter,
941
+ setWorkflowFilter,
942
+ setWorkflowIdFilter,
943
+ filter,
944
+ autoRefresh,
945
+ autoRefreshInterval
946
+ } = useExecutionsStore();
947
+ const [searchParams] = useSearchParams();
948
+ const workflowKeyParam = searchParams.get("workflowKey") ?? "";
949
+ const lastWorkflowKeyParam = useRef(null);
950
+ if (lastWorkflowKeyParam.current !== workflowKeyParam) {
951
+ lastWorkflowKeyParam.current = workflowKeyParam;
952
+ if (useExecutionsStore.getState().filter.workflowKey !== workflowKeyParam) {
953
+ setWorkflowFilter(workflowKeyParam);
954
+ }
955
+ }
956
+ const parityEnabled = useDurableParityEnabled();
957
+ const phaseC = usePhaseCFlags();
958
+ const [searchFilter, setSearchFilter] = useState(null);
959
+ const [allMatching, setAllMatching] = useState(false);
960
+ const [batchType, setBatchType] = useState(null);
961
+ const [batchDrawer, setBatchDrawer] = useState(null);
962
+ const { allowed: canExecuteRuns } = useFluidGridsPermission(
963
+ FLUIDGRIDS_PERMISSIONS.WORKFLOW_RUN_EXECUTE
964
+ );
965
+ const { allowed: canDeleteRuns } = useFluidGridsPermission(
966
+ FLUIDGRIDS_PERMISSIONS.WORKFLOW_RUN_DELETE
967
+ );
968
+ const { executions, total, loading, error, refetch, input } = useExecutionsWithStore({
969
+ durableParity: parityEnabled,
970
+ searchAttributes: phaseC.searchAttributes ? searchFilter : null
971
+ });
972
+ const inputKey = JSON.stringify({ ...input, limit: 0, offset: 0 });
973
+ useEffect(() => setAllMatching(false), [inputKey]);
974
+ const batchOperations = useMemo(
975
+ () => allowedBatchOperations(
976
+ {
977
+ runControl: phaseC.runControl,
978
+ runControlDurable: phaseC.runControlDurable,
979
+ runArchival: phaseC.runArchival
980
+ },
981
+ { canExecute: canExecuteRuns, canDelete: canDeleteRuns }
982
+ ),
983
+ [
984
+ phaseC.runControl,
985
+ phaseC.runControlDurable,
986
+ phaseC.runArchival,
987
+ canExecuteRuns,
988
+ canDeleteRuns
989
+ ]
990
+ );
991
+ const statusCounts = useRunStatusCounts(input, parityEnabled);
992
+ const refetchCounts = statusCounts.refetch;
993
+ const {
994
+ stats,
995
+ loading: statsLoading,
996
+ refetch: refetchStats
997
+ } = useExecutionStats();
998
+ const { workflows } = useWorkflowDefinitions({ limit: 100 });
999
+ const {
1000
+ cancelExecution,
1001
+ pauseExecution,
1002
+ resumeExecution,
1003
+ retryExecution,
1004
+ retryExecutions
1005
+ } = useExecutionActions();
1006
+ useAutoRefresh(
1007
+ useCallback(() => {
1008
+ refetch();
1009
+ refetchStats();
1010
+ void refetchCounts();
1011
+ }, [refetch, refetchStats, refetchCounts]),
1012
+ autoRefresh,
1013
+ autoRefreshInterval
1014
+ );
1015
+ const executionStats = useMemo(() => {
1016
+ if (!stats) return null;
1017
+ return {
1018
+ total: stats.runsCountTotal,
1019
+ running: stats.runsRunningCount,
1020
+ completed: stats.runsCompletedCount,
1021
+ failed: stats.runsFailedCount,
1022
+ cancelled: stats.runsCancelledCount ?? 0,
1023
+ pending: stats.runsPendingCount,
1024
+ waiting: stats.runsWaitingCount ?? 0,
1025
+ successRate: stats.workflowSuccessRate,
1026
+ avgDuration: stats.avgWorkflowExecutionTimeSeconds
1027
+ };
1028
+ }, [stats]);
1029
+ const workflowOptions = useMemo(
1030
+ () => workflows.map((w) => ({
1031
+ workflowKey: w.workflowKey,
1032
+ name: w.name
1033
+ })),
1034
+ [workflows]
1035
+ );
1036
+ const workflowNames = useMemo(() => {
1037
+ const map = {};
1038
+ for (const w of workflows) map[w.workflowKey] = w.name;
1039
+ return map;
1040
+ }, [workflows]);
1041
+ const handleRowClick = (execution) => {
1042
+ setSelectedExecution(execution);
1043
+ navigate(`../runs/${execution.id}`);
1044
+ };
1045
+ const handleRetry = async (execution) => {
1046
+ const result = await retryExecution(execution);
1047
+ if (!result.ok) {
1048
+ toast.error(
1049
+ tr(
1050
+ "fluidgrids.executions.executionsList.retryFailed",
1051
+ "Could not start a new run"
1052
+ ),
1053
+ { description: result.message }
1054
+ );
1055
+ return;
1056
+ }
1057
+ toast.success(
1058
+ tr(
1059
+ "fluidgrids.executions.executionsList.retryStarted",
1060
+ "New run started"
1061
+ )
1062
+ );
1063
+ navigate(`../runs/${result.run.runId}`);
1064
+ };
1065
+ const handleAction = async (action, execution) => {
1066
+ switch (action) {
1067
+ case "cancel":
1068
+ await cancelExecution(execution.id);
1069
+ break;
1070
+ case "pause":
1071
+ await pauseExecution(execution.id);
1072
+ break;
1073
+ case "resume":
1074
+ await resumeExecution(execution.id);
1075
+ break;
1076
+ case "retry":
1077
+ await handleRetry(execution);
1078
+ break;
1079
+ case "view":
1080
+ navigate(`../runs/${execution.id}`);
1081
+ break;
1082
+ }
1083
+ refetch();
1084
+ };
1085
+ const handleFilterByStatus = (status) => {
1086
+ setStatusFilter(status);
1087
+ };
1088
+ const handleBulkRetry = async () => {
1089
+ if (selection.runIds.length === 0) return;
1090
+ const retried = await retryExecutions(selection.runIds.slice(0, 10));
1091
+ if (retried.length === 0) {
1092
+ toast.error(
1093
+ tr(
1094
+ "fluidgrids.executions.executionsList.bulkRetryFailed",
1095
+ "Could not retry the selected runs"
1096
+ )
1097
+ );
1098
+ return;
1099
+ }
1100
+ toast.success(
1101
+ tr(
1102
+ "fluidgrids.executions.executionsList.bulkRetryStarted",
1103
+ `${retried.length} new run${retried.length === 1 ? "" : "s"} started`
1104
+ )
1105
+ );
1106
+ clearSelection();
1107
+ refetch();
1108
+ };
1109
+ const handleBulkAction = async (action) => {
1110
+ if (selection.runIds.length === 0) return;
1111
+ const promises = selection.runIds.map((id) => {
1112
+ switch (action) {
1113
+ case "cancel":
1114
+ return cancelExecution(id);
1115
+ case "pause":
1116
+ return pauseExecution(id);
1117
+ case "resume":
1118
+ return resumeExecution(id);
1119
+ }
1120
+ });
1121
+ const results = await Promise.allSettled(promises);
1122
+ const failures = results.filter((r) => r.status === "rejected");
1123
+ if (failures.length > 0) {
1124
+ console.error(
1125
+ `Bulk action partially failed: ${failures.length}/${results.length} failed`
1126
+ );
1127
+ }
1128
+ clearSelection();
1129
+ refetch();
1130
+ };
1131
+ return /* @__PURE__ */ jsxs("div", { className: "p-4 sm:p-6 bg-background-primary min-h-screen", children: [
1132
+ /* @__PURE__ */ jsxs(
1133
+ "div",
1134
+ {
1135
+ className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6",
1136
+ "data-tour": "fluidgrids-runs-header",
1137
+ children: [
1138
+ /* @__PURE__ */ jsxs("div", { children: [
1139
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl sm:text-3xl font-bold text-text-primary", children: tr("fluidgrids.executions.executionsList.title", "Executions") }),
1140
+ /* @__PURE__ */ jsx("p", { className: "text-text-secondary mt-1", children: tr(
1141
+ "fluidgrids.executions.executionsList.subtitle",
1142
+ "Monitor and manage your workflow runs"
1143
+ ) }),
1144
+ /* @__PURE__ */ jsx(FluidGridsDocsLink, { topic: "runs", className: "mt-1" })
1145
+ ] }),
1146
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
1147
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center bg-background-secondary rounded-lg border border-border-secondary p-1", children: [
1148
+ /* @__PURE__ */ jsx(
1149
+ "button",
1150
+ {
1151
+ onClick: () => setViewMode("table"),
1152
+ className: `p-2 rounded ${viewMode === "table" ? "bg-background-tertiary text-text-primary" : "text-text-secondary hover:text-text-primary"}`,
1153
+ title: tr(
1154
+ "fluidgrids.executions.executionsList.tableViewTooltip",
1155
+ "Table View"
1156
+ ),
1157
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mdi:table", width: 18 })
1158
+ }
1159
+ ),
1160
+ /* @__PURE__ */ jsx(
1161
+ "button",
1162
+ {
1163
+ onClick: () => setViewMode("cards"),
1164
+ className: `p-2 rounded ${viewMode === "cards" ? "bg-background-tertiary text-text-primary" : "text-text-secondary hover:text-text-primary"}`,
1165
+ title: tr(
1166
+ "fluidgrids.executions.executionsList.cardViewTooltip",
1167
+ "Card View"
1168
+ ),
1169
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mdi:view-grid", width: 18 })
1170
+ }
1171
+ ),
1172
+ /* @__PURE__ */ jsx(
1173
+ "button",
1174
+ {
1175
+ onClick: () => setViewMode("timeline"),
1176
+ className: `p-2 rounded ${viewMode === "timeline" ? "bg-background-tertiary text-text-primary" : "text-text-secondary hover:text-text-primary"}`,
1177
+ title: tr(
1178
+ "fluidgrids.executions.executionsList.timelineViewTooltip",
1179
+ "Timeline View"
1180
+ ),
1181
+ children: /* @__PURE__ */ jsx(Icon, { icon: "mdi:timeline", width: 18 })
1182
+ }
1183
+ )
1184
+ ] }),
1185
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", onClick: () => navigate("../list"), children: [
1186
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:arrow-left", className: "mr-2", width: 16 }),
1187
+ tr(
1188
+ "fluidgrids.executions.executionsList.backToWorkflows",
1189
+ "Back to Workflows"
1190
+ )
1191
+ ] })
1192
+ ] })
1193
+ ]
1194
+ }
1195
+ ),
1196
+ /* @__PURE__ */ jsx("div", { className: "mb-6", "data-tour": "fluidgrids-runs-stats", children: /* @__PURE__ */ jsx(
1197
+ ExecutionStats,
1198
+ {
1199
+ stats: executionStats,
1200
+ loading: statsLoading,
1201
+ onFilterByStatus: handleFilterByStatus
1202
+ }
1203
+ ) }),
1204
+ /* @__PURE__ */ jsx("div", { className: "mb-6", "data-tour": "fluidgrids-runs-filters", children: /* @__PURE__ */ jsx(ExecutionFilters, { workflows: workflowOptions, onRefresh: refetch }) }),
1205
+ parityEnabled && /* @__PURE__ */ jsx(
1206
+ RunsParityBar,
1207
+ {
1208
+ workflowId: filter.workflowId,
1209
+ onWorkflowIdChange: setWorkflowIdFilter,
1210
+ total: statusCounts.total,
1211
+ counts: statusCounts.counts,
1212
+ countsError: statusCounts.error,
1213
+ selectedStatus: filter.status,
1214
+ onSelectStatus: handleFilterByStatus
1215
+ }
1216
+ ),
1217
+ phaseC.searchAttributes && /* @__PURE__ */ jsx(
1218
+ SearchAttributeFilterBar,
1219
+ {
1220
+ applied: searchFilter,
1221
+ onApply: setSearchFilter,
1222
+ conflicts: searchFilter ? searchFilterConflicts(input) : []
1223
+ }
1224
+ ),
1225
+ phaseC.runControl && selection.runIds.length === 0 && /* @__PURE__ */ jsx("div", { className: "mb-4 flex justify-end", children: /* @__PURE__ */ jsxs(
1226
+ Button,
1227
+ {
1228
+ variant: "ghost",
1229
+ size: "sm",
1230
+ onClick: () => setBatchDrawer({ job: null }),
1231
+ "data-testid": "batch-jobs-button",
1232
+ children: [
1233
+ /* @__PURE__ */ jsx(
1234
+ Icon,
1235
+ {
1236
+ icon: "mdi:layers-triple-outline",
1237
+ className: "mr-1",
1238
+ width: 14
1239
+ }
1240
+ ),
1241
+ tr("fluidgrids.runControl.batch.jobs", "Batch jobs")
1242
+ ]
1243
+ }
1244
+ ) }),
1245
+ selection.runIds.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mb-4 p-3 bg-action-primary-bg/10 rounded-lg flex flex-wrap items-center gap-3", children: [
1246
+ /* @__PURE__ */ jsxs("span", { className: "text-sm text-text-primary", children: [
1247
+ selection.runIds.length,
1248
+ " ",
1249
+ selection.runIds.length > 1 ? tr(
1250
+ "fluidgrids.executions.executionsList.selectionCountPlural",
1251
+ "executions selected"
1252
+ ) : tr(
1253
+ "fluidgrids.executions.executionsList.selectionCountSingular",
1254
+ "execution selected"
1255
+ )
1256
+ ] }),
1257
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
1258
+ /* @__PURE__ */ jsxs(
1259
+ Button,
1260
+ {
1261
+ "data-agent": "confirm",
1262
+ variant: "outline",
1263
+ size: "sm",
1264
+ onClick: () => void handleBulkRetry(),
1265
+ children: [
1266
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:refresh", className: "mr-1", width: 14 }),
1267
+ tr(
1268
+ "fluidgrids.executions.executionsList.bulkRetryButton",
1269
+ "Retry selected"
1270
+ )
1271
+ ]
1272
+ }
1273
+ ),
1274
+ /* @__PURE__ */ jsxs(
1275
+ Button,
1276
+ {
1277
+ "data-agent": "confirm",
1278
+ variant: "outline",
1279
+ size: "sm",
1280
+ onClick: () => handleBulkAction("cancel"),
1281
+ children: [
1282
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:stop", className: "mr-1", width: 14 }),
1283
+ tr(
1284
+ "fluidgrids.executions.executionsList.bulkCancelButton",
1285
+ "Cancel"
1286
+ )
1287
+ ]
1288
+ }
1289
+ ),
1290
+ /* @__PURE__ */ jsxs(
1291
+ Button,
1292
+ {
1293
+ "data-agent": "confirm",
1294
+ variant: "outline",
1295
+ size: "sm",
1296
+ onClick: () => handleBulkAction("pause"),
1297
+ children: [
1298
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:pause", className: "mr-1", width: 14 }),
1299
+ tr(
1300
+ "fluidgrids.executions.executionsList.bulkPauseButton",
1301
+ "Pause"
1302
+ )
1303
+ ]
1304
+ }
1305
+ ),
1306
+ /* @__PURE__ */ jsxs(
1307
+ Button,
1308
+ {
1309
+ "data-agent": "confirm",
1310
+ variant: "outline",
1311
+ size: "sm",
1312
+ onClick: () => handleBulkAction("resume"),
1313
+ children: [
1314
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:play", className: "mr-1", width: 14 }),
1315
+ tr(
1316
+ "fluidgrids.executions.executionsList.bulkResumeButton",
1317
+ "Resume"
1318
+ )
1319
+ ]
1320
+ }
1321
+ )
1322
+ ] }),
1323
+ phaseC.runControl && /* @__PURE__ */ jsxs(Fragment, { children: [
1324
+ selection.allSelected && total > selection.runIds.length && /* @__PURE__ */ jsx(
1325
+ "button",
1326
+ {
1327
+ type: "button",
1328
+ onClick: () => setAllMatching((current) => !current),
1329
+ className: "text-sm text-action-primary-text hover:underline",
1330
+ "data-testid": "select-all-matching",
1331
+ "aria-pressed": allMatching,
1332
+ children: allMatching ? tr(
1333
+ "fluidgrids.runControl.batch.allMatchingSelected",
1334
+ "All {{count}} matching runs are selected",
1335
+ { count: total }
1336
+ ) : tr(
1337
+ "fluidgrids.runControl.batch.selectAllMatching",
1338
+ "Select all {{count}} matching runs",
1339
+ { count: total }
1340
+ )
1341
+ }
1342
+ ),
1343
+ /* @__PURE__ */ jsx(
1344
+ BatchMenu,
1345
+ {
1346
+ allowed: batchOperations,
1347
+ onPick: setBatchType,
1348
+ onShowJobs: () => setBatchDrawer({ job: null })
1349
+ }
1350
+ )
1351
+ ] }),
1352
+ /* @__PURE__ */ jsx(
1353
+ "button",
1354
+ {
1355
+ onClick: () => {
1356
+ setAllMatching(false);
1357
+ clearSelection();
1358
+ },
1359
+ className: "ml-auto text-sm text-text-secondary hover:text-text-primary",
1360
+ children: tr(
1361
+ "fluidgrids.executions.executionsList.clearSelection",
1362
+ "Clear selection"
1363
+ )
1364
+ }
1365
+ )
1366
+ ] }),
1367
+ error && /* @__PURE__ */ jsxs("div", { className: "mb-6 p-4 bg-[var(--color-status-error-bg-subtle)] border border-[var(--color-status-error-border)] rounded-lg", children: [
1368
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-[var(--color-status-error-text)]", children: [
1369
+ /* @__PURE__ */ jsx(Icon, { icon: "mdi:alert-circle", width: 20 }),
1370
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: tr(
1371
+ "fluidgrids.executions.executionsList.loadErrorTitle",
1372
+ "Error loading executions"
1373
+ ) })
1374
+ ] }),
1375
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-[var(--color-status-error-text)]", children: error.message }),
1376
+ /* @__PURE__ */ jsx(
1377
+ Button,
1378
+ {
1379
+ variant: "outline",
1380
+ size: "sm",
1381
+ onClick: refetch,
1382
+ className: "mt-3 border-[var(--color-status-error-border)] text-[var(--color-status-error-text)] hover:bg-[var(--color-status-error-bg-subtle)]",
1383
+ children: tr("fluidgrids.executions.executionsList.retryButton", "Retry")
1384
+ }
1385
+ )
1386
+ ] }),
1387
+ viewMode === "table" && /* @__PURE__ */ jsx(
1388
+ ExecutionTable,
1389
+ {
1390
+ workflowNames,
1391
+ executions,
1392
+ loading,
1393
+ onRowClick: handleRowClick,
1394
+ onAction: handleAction
1395
+ }
1396
+ ),
1397
+ viewMode === "cards" && /* @__PURE__ */ jsx(
1398
+ ExecutionCards,
1399
+ {
1400
+ workflowNames,
1401
+ executions,
1402
+ loading,
1403
+ onCardClick: handleRowClick,
1404
+ onAction: handleAction
1405
+ }
1406
+ ),
1407
+ viewMode === "timeline" && /* @__PURE__ */ jsx(
1408
+ ExecutionTimeline,
1409
+ {
1410
+ executions,
1411
+ loading,
1412
+ onItemClick: handleRowClick,
1413
+ onAction: handleAction
1414
+ }
1415
+ ),
1416
+ /* @__PURE__ */ jsx(Pagination, { totalItems: total }),
1417
+ batchType && /* @__PURE__ */ jsx(
1418
+ BatchOperationDialog,
1419
+ {
1420
+ type: batchType,
1421
+ input,
1422
+ selection: { runIds: selection.runIds, allMatching },
1423
+ onClose: () => setBatchType(null),
1424
+ onStarted: (job) => {
1425
+ setBatchType(null);
1426
+ setAllMatching(false);
1427
+ clearSelection();
1428
+ setBatchDrawer({ job });
1429
+ }
1430
+ }
1431
+ ),
1432
+ batchDrawer && /* @__PURE__ */ jsx(
1433
+ BatchProgressDrawer,
1434
+ {
1435
+ job: batchDrawer.job,
1436
+ open: true,
1437
+ onOpenChange: (open) => {
1438
+ if (open) return;
1439
+ setBatchDrawer(null);
1440
+ refetch();
1441
+ },
1442
+ canCancel: canExecuteRuns
1443
+ }
1444
+ )
1445
+ ] });
1446
+ }
1447
+
1448
+ export { ExecutionsListPage };