@burdenoff/fluidgrids-fe-sdk 2026.1111.17 → 2026.1111.18

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 (31) hide show
  1. package/dist/{AiStudioPage-kJHSsIcj.js → AiStudioPage-BHVEU4Wp.js} +2 -2
  2. package/dist/{SettingsPage-CgkRlCM5.js → SettingsPage-CJLZvv4J.js} +1485 -1484
  3. package/dist/{TemplatePreview-GthzCOSo.js → TemplatePreview-FayF6jxh.js} +1 -1
  4. package/dist/{WorkflowBuilder-DEi_9YOb.js → WorkflowBuilder-CPnXEL5J.js} +1 -1
  5. package/dist/{WorkflowBuilder-iPqXgbYr.js → WorkflowBuilder-YXRysvZc.js} +2 -2
  6. package/dist/WorkflowShareDialog-mbIwRQUY.js +168 -0
  7. package/dist/{WorkflowsList-CA20dVIl.js → WorkflowsList-DiiGyZkT.js} +2 -1
  8. package/dist/{agentTools-CkRJB7r5.js → agentTools-8s_25Q_F.js} +1 -1
  9. package/dist/{canvasNoteTools-CQIc7Oxg.js → canvasNoteTools-B6pYARgU.js} +1 -1
  10. package/dist/code-editor-field-9-GdVE_8.js +111 -0
  11. package/dist/{flow-builder-y2Y1iG4Y.js → flow-builder-CZDKRc6f.js} +245 -8638
  12. package/dist/{index-W2o__Nm6.d.ts → index-6JR2vD9J.d.ts} +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +13 -11
  15. package/dist/{node-detail-view-B91kXzIy.js → node-detail-view-CV4ZRwqh.js} +4 -2
  16. package/dist/node-editor-tab-body-DU-dh35X.js +174 -0
  17. package/dist/observability.d.ts +1 -1
  18. package/dist/popover-BJHvrFGR.js +472 -0
  19. package/dist/{quick-config-panel-DggPl1vY.js → quick-config-panel-ws3pLvbt.js} +2 -1
  20. package/dist/schedule-panel-C0mfGQSD.js +7542 -0
  21. package/dist/settings.js +1 -1
  22. package/dist/sidebar-node-editor-CR1PiZO-.js +177 -0
  23. package/dist/templates.js +1 -1
  24. package/dist/{useAgentDefinitions-Dpz0HDTq.js → useAgentDefinitions-B0dam3_o.js} +1 -396
  25. package/dist/useConnections-Cs7LCWEM.js +401 -0
  26. package/dist/{WorkflowShareDialog-B_gn968c.js → useWorkflowCodeExport-Bu2QYJzA.js} +4 -161
  27. package/dist/workflow-builder.js +2 -2
  28. package/dist/workflow-pages.d.ts +1 -1
  29. package/dist/workflow-pages.js +5 -5
  30. package/dist/workflows-list.js +1 -1
  31. package/package.json +1 -1
@@ -13,7 +13,8 @@ import { KF as WorkflowSearchAttributeType } from "./graphql-types-CDKVWH2R.js";
13
13
  import { H as useFlowStore } from "./node-operation-selection--oy-yEq_.js";
14
14
  import { At as useSaveStatusStore, Qt as findSaveConflict } from "./FluidGridsProvider-dvZSNtSW.js";
15
15
  import { a as useLogStreamDestinations, i as useLogStreamDestinationActions } from "./useObservability-CfEw3ZFD.js";
16
- import { Wt as useWorkflowDefinitionQuery, a as useConnections, n as useAgentDefinitions, qt as useWorkflowDefinitionsQuery, zt as useUpdateWorkflowDefinitionMutation } from "./useAgentDefinitions-Dpz0HDTq.js";
16
+ import { n as useConnections } from "./useConnections-Cs7LCWEM.js";
17
+ import { Nt as useUpdateWorkflowDefinitionMutation, Rt as useWorkflowDefinitionQuery, Vt as useWorkflowDefinitionsQuery, n as useAgentDefinitions } from "./useAgentDefinitions-B0dam3_o.js";
17
18
  import { a as formToInput$2, i as emptyAgentForm, n as agentToForm, o as grantDays, r as describeRule, t as AgentFormError } from "./agent-form-Bfvm32j1.js";
18
19
  import { i as getGlobalStoreClient } from "./node-registry-refresh-Dy0uRg9f.js";
19
20
  import { t as FluidGridsDocsLink } from "./FluidGridsDocsLink-JxkBpOeb.js";
@@ -30,437 +31,6 @@ import { print } from "graphql";
30
31
  import { clearGraphqlFetchResponseCache, getStoredOrganizationId, getStoredProjectId, getWorkspaceTokenPayload, graphqlFetchPinned, isWorkspaceTokenValidForContext, prepareWorkspaceRequest } from "@burdenoff/fe-libs/shared";
31
32
  import { resolveExternalGatewayUrls } from "@burdenoff/fe-libs/shared/config";
32
33
 
33
- // src/modules/agent-definitions/components/AgentsTab.tsx
34
- /**
35
- * Settings → Agents (BOFF-9962): FluidGrids agents with their own identity.
36
- *
37
- * Builders create an agent, choose the tools it may call (node type and operations) and the
38
- * connections those tools may use, and can require approval for every tool call. Each person
39
- * lets the agent act for them with a time-boxed grant (30 days by default, 365 at most) and can
40
- * withdraw it at any time; admins can revoke anyone's grant. An AI Agent node that names the agent
41
- * runs its tools as "agent for user", and every call is recorded under both identities. Giving an
42
- * agent more access withdraws other people's grants until they grant again.
43
- */
44
- function errorMessage$2(err) {
45
- if (err instanceof AgentFormError) return err.message;
46
- return describeOperationError(err);
47
- }
48
- function AgentEditor({ initial, onCancel, onSave }) {
49
- const tr = useTr();
50
- const [form, setForm] = useState(initial);
51
- const [busy, setBusy] = useState(false);
52
- const { connections, loading: connectionsLoading } = useConnections();
53
- const setTool = (index, patch) => setForm((current) => ({
54
- ...current,
55
- tools: current.tools.map((row, i) => i === index ? {
56
- ...row,
57
- ...patch
58
- } : row)
59
- }));
60
- const toggleConnection = (id, on) => setForm((current) => ({
61
- ...current,
62
- connectionScope: on ? [.../* @__PURE__ */ new Set([...current.connectionScope, id])] : current.connectionScope.filter((value) => value !== id)
63
- }));
64
- const submit = async () => {
65
- setBusy(true);
66
- try {
67
- await onSave(form);
68
- } catch (err) {
69
- toast.error(errorMessage$2(err));
70
- } finally {
71
- setBusy(false);
72
- }
73
- };
74
- return /* @__PURE__ */ jsxs("div", {
75
- className: "rounded-lg border border-border-secondary bg-background-primary p-4 space-y-4",
76
- "data-testid": "agent-definition-editor",
77
- children: [
78
- /* @__PURE__ */ jsxs("div", {
79
- className: "grid gap-3 sm:grid-cols-2",
80
- children: [/* @__PURE__ */ jsxs("div", {
81
- className: "space-y-1",
82
- children: [/* @__PURE__ */ jsx(Label, {
83
- htmlFor: "agent-name",
84
- children: tr("fluidgrids.agents.name", "Name")
85
- }), /* @__PURE__ */ jsx(Input, {
86
- id: "agent-name",
87
- value: form.name,
88
- onChange: (e) => setForm({
89
- ...form,
90
- name: e.target.value
91
- }),
92
- "data-testid": "agent-definition-name"
93
- })]
94
- }), /* @__PURE__ */ jsxs("div", {
95
- className: "flex items-end gap-2",
96
- children: [/* @__PURE__ */ jsx(Switch, {
97
- id: "agent-active",
98
- checked: form.status === "ACTIVE",
99
- onCheckedChange: (on) => setForm({
100
- ...form,
101
- status: on ? "ACTIVE" : "DISABLED"
102
- })
103
- }), /* @__PURE__ */ jsx(Label, {
104
- htmlFor: "agent-active",
105
- children: tr("fluidgrids.agents.active", "Active")
106
- })]
107
- })]
108
- }),
109
- /* @__PURE__ */ jsxs("div", {
110
- className: "space-y-1",
111
- children: [/* @__PURE__ */ jsx(Label, {
112
- htmlFor: "agent-description",
113
- children: tr("fluidgrids.agents.description", "Description")
114
- }), /* @__PURE__ */ jsx(Textarea, {
115
- id: "agent-description",
116
- value: form.description,
117
- onChange: (e) => setForm({
118
- ...form,
119
- description: e.target.value
120
- }),
121
- rows: 2
122
- })]
123
- }),
124
- /* @__PURE__ */ jsxs("div", {
125
- className: "space-y-2",
126
- children: [
127
- /* @__PURE__ */ jsx(Label, { children: tr("fluidgrids.agents.tools", "Tools the agent may call") }),
128
- /* @__PURE__ */ jsx("p", {
129
- className: "text-xs text-text-secondary",
130
- children: tr("fluidgrids.agents.toolsHelp", "A node type (for example store/slack) and the operations of it the agent may call, separated by commas. Leave the operations empty to allow every operation of that node type. Nothing else can be called.")
131
- }),
132
- form.tools.map((row, index) => /* @__PURE__ */ jsxs("div", {
133
- className: "flex flex-wrap items-center gap-2",
134
- "data-testid": `agent-definition-tool-${index}`,
135
- children: [
136
- /* @__PURE__ */ jsx(Input, {
137
- className: "w-48",
138
- placeholder: "store/slack",
139
- value: row.nodeType,
140
- onChange: (e) => setTool(index, { nodeType: e.target.value }),
141
- "aria-label": tr("fluidgrids.agents.nodeType", "Node type")
142
- }),
143
- /* @__PURE__ */ jsx(Input, {
144
- className: "min-w-48 flex-1",
145
- placeholder: "postMessage, listChannels",
146
- value: row.operations,
147
- onChange: (e) => setTool(index, { operations: e.target.value }),
148
- "aria-label": tr("fluidgrids.agents.operations", "Operations")
149
- }),
150
- /* @__PURE__ */ jsx(Button$1, {
151
- variant: "ghost",
152
- size: "sm",
153
- onClick: () => setForm({
154
- ...form,
155
- tools: form.tools.filter((_, i) => i !== index)
156
- }),
157
- "aria-label": tr("fluidgrids.agents.removeTool", "Remove tool"),
158
- children: /* @__PURE__ */ jsx(Icon, {
159
- icon: "mdi:close",
160
- width: 16
161
- })
162
- })
163
- ]
164
- }, index)),
165
- /* @__PURE__ */ jsxs(Button$1, {
166
- variant: "outline",
167
- size: "sm",
168
- onClick: () => setForm({
169
- ...form,
170
- tools: [...form.tools, {
171
- nodeType: "",
172
- operations: ""
173
- }]
174
- }),
175
- children: [/* @__PURE__ */ jsx(Icon, {
176
- icon: "mdi:plus",
177
- width: 16
178
- }), tr("fluidgrids.agents.addTool", "Add tool")]
179
- })
180
- ]
181
- }),
182
- /* @__PURE__ */ jsxs("div", {
183
- className: "space-y-2",
184
- children: [/* @__PURE__ */ jsx(Label, { children: tr("fluidgrids.agents.connections", "Connections the agent's tools may use") }), connectionsLoading ? /* @__PURE__ */ jsx("p", {
185
- className: "text-xs text-text-secondary",
186
- children: tr("fluidgrids.agents.loadingConnections", "Loading connections…")
187
- }) : connections.length === 0 ? /* @__PURE__ */ jsx("p", {
188
- className: "text-xs text-text-secondary",
189
- children: tr("fluidgrids.agents.noConnections", "This workspace has no connections yet.")
190
- }) : /* @__PURE__ */ jsx("div", {
191
- className: "grid gap-1 sm:grid-cols-2",
192
- children: connections.map((connection) => /* @__PURE__ */ jsxs("label", {
193
- className: "flex items-center gap-2 text-sm text-text-primary",
194
- children: [
195
- /* @__PURE__ */ jsx(Checkbox, {
196
- checked: form.connectionScope.includes(connection.id),
197
- onCheckedChange: (on) => toggleConnection(connection.id, on === true)
198
- }),
199
- /* @__PURE__ */ jsx("span", { children: connection.name }),
200
- /* @__PURE__ */ jsx("span", {
201
- className: "text-xs text-text-tertiary",
202
- children: connection.provider?.name ?? connection.providerId
203
- })
204
- ]
205
- }, connection.id))
206
- })]
207
- }),
208
- /* @__PURE__ */ jsxs("div", {
209
- className: "flex items-center gap-2",
210
- children: [/* @__PURE__ */ jsx(Switch, {
211
- id: "agent-approval",
212
- checked: form.requireToolApproval,
213
- onCheckedChange: (on) => setForm({
214
- ...form,
215
- requireToolApproval: on
216
- })
217
- }), /* @__PURE__ */ jsx(Label, {
218
- htmlFor: "agent-approval",
219
- children: tr("fluidgrids.agents.requireApproval", "Every tool call waits for a person's approval")
220
- })]
221
- }),
222
- /* @__PURE__ */ jsxs("div", {
223
- className: "flex gap-2",
224
- children: [/* @__PURE__ */ jsx(Button$1, {
225
- onClick: () => void submit(),
226
- disabled: busy,
227
- "data-testid": "agent-definition-save",
228
- children: tr("fluidgrids.agents.save", "Save agent")
229
- }), /* @__PURE__ */ jsx(Button$1, {
230
- variant: "outline",
231
- onClick: onCancel,
232
- disabled: busy,
233
- children: tr("fluidgrids.agents.cancel", "Cancel")
234
- })]
235
- })
236
- ]
237
- });
238
- }
239
- function AgentGrants({ agent, loadGrants, onGrant, onRevoke }) {
240
- const tr = useTr();
241
- const [grants, setGrants] = useState(null);
242
- const [days, setDays] = useState(String(30));
243
- const [busy, setBusy] = useState(false);
244
- const load = useCallback(async () => {
245
- try {
246
- setGrants(await loadGrants(agent.id));
247
- } catch (err) {
248
- setGrants([]);
249
- toast.error(errorMessage$2(err));
250
- }
251
- }, [agent.id, loadGrants]);
252
- useEffect(() => {
253
- load();
254
- }, [load]);
255
- const run = async (action, done) => {
256
- setBusy(true);
257
- try {
258
- await action();
259
- toast.success(done);
260
- await load();
261
- } catch (err) {
262
- toast.error(errorMessage$2(err));
263
- } finally {
264
- setBusy(false);
265
- }
266
- };
267
- const mine = grants?.find((grant) => grant.isMine && grant.status === "ACTIVE");
268
- return /* @__PURE__ */ jsxs("div", {
269
- className: "space-y-2",
270
- "data-testid": `agent-grants-${agent.id}`,
271
- children: [/* @__PURE__ */ jsx("div", {
272
- className: "flex flex-wrap items-center gap-2",
273
- children: mine ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
274
- className: "text-sm text-text-primary",
275
- children: mine.expiresAt ? tr("fluidgrids.agents.myGrantUntil", "This agent may act for you until {{date}}.", { date: new Date(mine.expiresAt).toLocaleDateString() }) : tr("fluidgrids.agents.myGrant", "This agent may act for you.")
276
- }), /* @__PURE__ */ jsx(Button$1, {
277
- variant: "outline",
278
- size: "sm",
279
- disabled: busy,
280
- onClick: () => void run(() => onRevoke(agent.id, null), tr("fluidgrids.agents.withdrawn", "Grant withdrawn")),
281
- "data-testid": "agent-grant-withdraw",
282
- children: tr("fluidgrids.agents.withdraw", "Withdraw my grant")
283
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
284
- /* @__PURE__ */ jsx(Label, {
285
- htmlFor: `grant-days-${agent.id}`,
286
- className: "text-sm",
287
- children: tr("fluidgrids.agents.grantFor", "Let it act for me for")
288
- }),
289
- /* @__PURE__ */ jsx(Input, {
290
- id: `grant-days-${agent.id}`,
291
- className: "w-20",
292
- type: "number",
293
- min: 1,
294
- max: 365,
295
- value: days,
296
- onChange: (e) => setDays(e.target.value)
297
- }),
298
- /* @__PURE__ */ jsx("span", {
299
- className: "text-sm text-text-secondary",
300
- children: tr("fluidgrids.agents.days", "days")
301
- }),
302
- /* @__PURE__ */ jsx(Button$1, {
303
- size: "sm",
304
- disabled: busy || agent.status !== "ACTIVE",
305
- onClick: () => void run(async () => onGrant(agent.id, grantDays(days)), tr("fluidgrids.agents.granted", "Grant given")),
306
- "data-testid": "agent-grant-give",
307
- children: tr("fluidgrids.agents.grant", "Grant")
308
- })
309
- ] })
310
- }), grants && grants.length > 0 ? /* @__PURE__ */ jsx("ul", {
311
- className: "space-y-1 text-xs text-text-secondary",
312
- children: grants.map((grant) => /* @__PURE__ */ jsxs("li", {
313
- className: "flex items-center gap-2",
314
- children: [
315
- /* @__PURE__ */ jsx("span", {
316
- className: "font-mono",
317
- children: grant.userId
318
- }),
319
- /* @__PURE__ */ jsx(Badge, {
320
- variant: "outline",
321
- children: grant.status
322
- }),
323
- grant.expiresAt ? /* @__PURE__ */ jsx("span", { children: tr("fluidgrids.agents.until", "until {{date}}", { date: new Date(grant.expiresAt).toLocaleDateString() }) }) : null,
324
- !grant.isMine && grant.status === "ACTIVE" ? /* @__PURE__ */ jsx(Button$1, {
325
- variant: "ghost",
326
- size: "sm",
327
- disabled: busy,
328
- onClick: () => void run(() => onRevoke(agent.id, grant.userId), tr("fluidgrids.agents.revoked", "Grant revoked")),
329
- children: tr("fluidgrids.agents.revoke", "Revoke")
330
- }) : null
331
- ]
332
- }, grant.userId))
333
- }) : null]
334
- });
335
- }
336
- function AgentsTab() {
337
- const tr = useTr();
338
- const { agents, loading, error, save, remove, grants, grant, revoke } = useAgentDefinitions();
339
- const [editing, setEditing] = useState(null);
340
- const onSave = async (form) => {
341
- const input = formToInput$2(form);
342
- await save(editing?.id ?? null, input);
343
- toast.success(tr("fluidgrids.agents.saved", "Agent saved"));
344
- setEditing(null);
345
- };
346
- const onDelete = async (agent) => {
347
- try {
348
- await remove(agent.id);
349
- toast.success(tr("fluidgrids.agents.deleted", "Agent deleted"));
350
- } catch (err) {
351
- toast.error(errorMessage$2(err));
352
- }
353
- };
354
- return /* @__PURE__ */ jsxs("div", {
355
- className: "space-y-4",
356
- "data-testid": "agents-tab",
357
- children: [
358
- /* @__PURE__ */ jsxs("div", {
359
- className: "flex flex-wrap items-start justify-between gap-2",
360
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
361
- className: "text-base font-semibold text-text-primary",
362
- children: tr("fluidgrids.agents.title", "Agents")
363
- }), /* @__PURE__ */ jsx("p", {
364
- className: "text-sm text-text-secondary",
365
- children: tr("fluidgrids.agents.subtitle", "Give an AI Agent node an identity of its own. It can only call the tools and connections you allow, only for people who granted it, and every tool call is recorded under the agent and the person.")
366
- })] }), !editing ? /* @__PURE__ */ jsxs(Button$1, {
367
- onClick: () => setEditing({
368
- id: null,
369
- form: emptyAgentForm()
370
- }),
371
- "data-testid": "agent-definition-new",
372
- children: [/* @__PURE__ */ jsx(Icon, {
373
- icon: "mdi:plus",
374
- width: 16
375
- }), tr("fluidgrids.agents.new", "New agent")]
376
- }) : null]
377
- }),
378
- editing ? /* @__PURE__ */ jsx(AgentEditor, {
379
- initial: editing.form,
380
- onCancel: () => setEditing(null),
381
- onSave
382
- }) : null,
383
- error ? /* @__PURE__ */ jsx("p", {
384
- className: "text-sm text-destructive",
385
- children: errorMessage$2(error)
386
- }) : null,
387
- loading ? /* @__PURE__ */ jsx("p", {
388
- className: "text-sm text-text-secondary",
389
- children: tr("fluidgrids.agents.loading", "Loading agents…")
390
- }) : agents.length === 0 && !error ? /* @__PURE__ */ jsx("p", {
391
- className: "text-sm text-text-secondary",
392
- children: tr("fluidgrids.agents.empty", "No agents yet.")
393
- }) : /* @__PURE__ */ jsx("ul", {
394
- className: "space-y-3",
395
- children: agents.map((agent) => /* @__PURE__ */ jsxs("li", {
396
- className: "rounded-lg border border-border-secondary bg-background-secondary p-3 space-y-2",
397
- "data-testid": `agent-definition-${agent.id}`,
398
- children: [
399
- /* @__PURE__ */ jsxs("div", {
400
- className: "flex flex-wrap items-center justify-between gap-2",
401
- children: [/* @__PURE__ */ jsxs("div", {
402
- className: "flex items-center gap-2",
403
- children: [
404
- /* @__PURE__ */ jsx(Icon, {
405
- icon: "mdi:robot-outline",
406
- width: 18
407
- }),
408
- /* @__PURE__ */ jsx("span", {
409
- className: "font-medium text-text-primary",
410
- children: agent.name
411
- }),
412
- /* @__PURE__ */ jsx(Badge, {
413
- variant: "outline",
414
- children: agent.status
415
- }),
416
- agent.requireToolApproval ? /* @__PURE__ */ jsx(Badge, {
417
- variant: "outline",
418
- children: tr("fluidgrids.agents.approvalBadge", "Approval required")
419
- }) : null
420
- ]
421
- }), /* @__PURE__ */ jsxs("div", {
422
- className: "flex gap-1",
423
- children: [/* @__PURE__ */ jsx(Button$1, {
424
- variant: "ghost",
425
- size: "sm",
426
- onClick: () => setEditing({
427
- id: agent.id,
428
- form: agentToForm(agent)
429
- }),
430
- children: tr("fluidgrids.agents.edit", "Edit")
431
- }), /* @__PURE__ */ jsx(Button$1, {
432
- variant: "ghost",
433
- size: "sm",
434
- onClick: () => void onDelete(agent),
435
- children: tr("fluidgrids.agents.delete", "Delete")
436
- })]
437
- })]
438
- }),
439
- agent.description ? /* @__PURE__ */ jsx("p", {
440
- className: "text-sm text-text-secondary",
441
- children: agent.description
442
- }) : null,
443
- /* @__PURE__ */ jsx("div", {
444
- className: "text-xs text-text-secondary",
445
- children: agent.toolAllowlist.length > 0 ? agent.toolAllowlist.map(describeRule).join(" · ") : tr("fluidgrids.agents.noTools", "No tools allowed")
446
- }),
447
- /* @__PURE__ */ jsx("div", {
448
- className: "text-xs text-text-tertiary",
449
- children: tr("fluidgrids.agents.connectionCount", "{{count}} connections in scope", { count: agent.connectionScope.length })
450
- }),
451
- /* @__PURE__ */ jsx(AgentGrants, {
452
- agent,
453
- loadGrants: grants,
454
- onGrant: grant,
455
- onRevoke: revoke
456
- })
457
- ]
458
- }, agent.id))
459
- })
460
- ]
461
- });
462
- }
463
-
464
34
  // src/modules/action-policies/utils/policy-form.ts
465
35
  const POLICY_EFFECTS = [
466
36
  "REQUIRE_APPROVAL",
@@ -744,7 +314,7 @@ function needsAllowFallback(draft, others) {
744
314
  * rule; changing a policy or deciding an approval needs workflow publish rights.
745
315
  */
746
316
  const FIELD$1 = "rounded-md border border-border-secondary bg-background-secondary px-2 py-1.5 text-sm text-text-primary";
747
- function errorMessage$1(err) {
317
+ function errorMessage$2(err) {
748
318
  if (err instanceof PolicyFormError) return err.message;
749
319
  return describeOperationError(err);
750
320
  }
@@ -858,7 +428,7 @@ function PolicyEditor({ initial, conditionEditable, storedCondition, others, onC
858
428
  try {
859
429
  await action();
860
430
  } catch (err) {
861
- toast.error(errorMessage$1(err));
431
+ toast.error(errorMessage$2(err));
862
432
  } finally {
863
433
  setBusy(false);
864
434
  }
@@ -1213,7 +783,7 @@ function ApprovalsList({ approvals, onDecide }) {
1213
783
  await onDecide(approval.id, true);
1214
784
  toast.success(tr("fluidgrids.actionPolicies.approved", "Approved — the run continues"));
1215
785
  } catch (err) {
1216
- toast.error(errorMessage$1(err));
786
+ toast.error(errorMessage$2(err));
1217
787
  } finally {
1218
788
  setBusy(null);
1219
789
  }
@@ -1229,7 +799,7 @@ function ApprovalsList({ approvals, onDecide }) {
1229
799
  await onDecide(approval.id, false);
1230
800
  toast.success(tr("fluidgrids.actionPolicies.declined", "Declined — the node fails"));
1231
801
  } catch (err) {
1232
- toast.error(errorMessage$1(err));
802
+ toast.error(errorMessage$2(err));
1233
803
  } finally {
1234
804
  setBusy(null);
1235
805
  }
@@ -1356,7 +926,7 @@ function ActionPoliciesTab() {
1356
926
  try {
1357
927
  setPreview(await dryRun({ excludePolicyId: policy.id }));
1358
928
  } catch (err) {
1359
- toast.error(errorMessage$1(err));
929
+ toast.error(errorMessage$2(err));
1360
930
  }
1361
931
  },
1362
932
  children: tr("fluidgrids.actionPolicies.withoutIt", "Without it")
@@ -1389,7 +959,7 @@ function ActionPoliciesTab() {
1389
959
  await remove(policy.id);
1390
960
  toast.success(tr("fluidgrids.actionPolicies.deleted", "Policy deleted"));
1391
961
  } catch (err) {
1392
- toast.error(errorMessage$1(err));
962
+ toast.error(errorMessage$2(err));
1393
963
  }
1394
964
  },
1395
965
  children: confirmDelete === policy.id ? tr("fluidgrids.actionPolicies.confirmDelete", "Confirm delete") : tr("fluidgrids.actionPolicies.delete", "Delete")
@@ -2744,1190 +2314,1621 @@ function WorkflowNotificationsTab({ workflowKey }) {
2744
2314
  })
2745
2315
  })
2746
2316
  ]
2747
- }),
2748
- /* @__PURE__ */ jsxs("div", {
2749
- className: "flex flex-wrap items-center justify-between gap-3",
2750
- children: [/* @__PURE__ */ jsx("p", {
2751
- className: "text-xs text-text-secondary",
2752
- children: settings && !settings.customized ? tr("fluidgrids.settings.workflowNotifications.defaultsNote", "These are the defaults; nobody has changed them for this workflow yet.") : null
2753
- }), /* @__PURE__ */ jsx(Button$1, {
2754
- "data-agent": "confirm",
2755
- onClick: () => void handleSave(),
2756
- disabled: locked,
2757
- "data-testid": "workflow-notifications-save",
2758
- children: saving ? tr("fluidgrids.settings.execution.saving", "Saving…") : tr("fluidgrids.settings.execution.save", "Save")
2759
- })]
2317
+ }),
2318
+ /* @__PURE__ */ jsxs("div", {
2319
+ className: "flex flex-wrap items-center justify-between gap-3",
2320
+ children: [/* @__PURE__ */ jsx("p", {
2321
+ className: "text-xs text-text-secondary",
2322
+ children: settings && !settings.customized ? tr("fluidgrids.settings.workflowNotifications.defaultsNote", "These are the defaults; nobody has changed them for this workflow yet.") : null
2323
+ }), /* @__PURE__ */ jsx(Button$1, {
2324
+ "data-agent": "confirm",
2325
+ onClick: () => void handleSave(),
2326
+ disabled: locked,
2327
+ "data-testid": "workflow-notifications-save",
2328
+ children: saving ? tr("fluidgrids.settings.execution.saving", "Saving…") : tr("fluidgrids.settings.execution.save", "Save")
2329
+ })]
2330
+ })
2331
+ ]
2332
+ });
2333
+ }
2334
+
2335
+ // src/modules/settings/components/RunControlTab.tsx
2336
+ /**
2337
+ * Settings → Run control (BOFF-9906, Phase C): the workspace's activity pause
2338
+ * rules (run control), its search attribute definitions (search attributes),
2339
+ * the archive destination of finished runs (run archival) and the codec
2340
+ * server the run page decodes encoded payloads with (self-hosted workers).
2341
+ * Each card shows, and reads, only while its own flag is on; the tab shows
2342
+ * while any of them is.
2343
+ */
2344
+ /**
2345
+ * Whether the Run control tab has anything to show. Pause rules act on durable
2346
+ * activities, so the service refuses their writes unless durable functions are
2347
+ * on as well (`runControlDurable`, not `runControl` alone).
2348
+ */
2349
+ function hasRunControlSettings(flags) {
2350
+ return flags.runControlDurable || flags.searchAttributes || flags.runArchival || flags.selfHostedWorkers;
2351
+ }
2352
+ function RunControlTab({ flags }) {
2353
+ return /* @__PURE__ */ jsxs("div", {
2354
+ className: "space-y-6",
2355
+ "data-testid": "settings-run-control",
2356
+ children: [
2357
+ flags.runControlDurable && /* @__PURE__ */ jsx(PauseRulesCard, {}),
2358
+ flags.searchAttributes && /* @__PURE__ */ jsx(SearchAttributeDefinitionsCard, {}),
2359
+ flags.runArchival && /* @__PURE__ */ jsx(ArchiveSettingsCard, {}),
2360
+ flags.selfHostedWorkers && /* @__PURE__ */ jsx(CodecServerCard, {})
2361
+ ]
2362
+ });
2363
+ }
2364
+ function Card$1({ icon, title, description, children, testId }) {
2365
+ return /* @__PURE__ */ jsxs("section", {
2366
+ className: "rounded-lg border border-border-secondary bg-background-secondary p-4 sm:p-6",
2367
+ "data-testid": testId,
2368
+ children: [
2369
+ /* @__PURE__ */ jsxs("div", {
2370
+ className: "mb-1 flex items-center gap-2",
2371
+ children: [/* @__PURE__ */ jsx(Icon, {
2372
+ icon,
2373
+ width: 18,
2374
+ className: "text-text-secondary"
2375
+ }), /* @__PURE__ */ jsx("h3", {
2376
+ className: "text-base font-semibold text-text-primary",
2377
+ children: title
2378
+ })]
2379
+ }),
2380
+ /* @__PURE__ */ jsx("p", {
2381
+ className: "mb-4 text-sm text-text-secondary",
2382
+ children: description
2383
+ }),
2384
+ children
2385
+ ]
2386
+ });
2387
+ }
2388
+ function PauseRulesCard() {
2389
+ const tr = useTr();
2390
+ const rules = useResource("pause-rules", true, fetchPauseRules);
2391
+ const { allowed: canEdit } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE);
2392
+ const [form, setForm] = useState({
2393
+ activityType: "",
2394
+ workflowKey: "",
2395
+ reason: "",
2396
+ expiresAt: ""
2397
+ });
2398
+ const [busy, setBusy] = useState(false);
2399
+ const [error, setError] = useState(null);
2400
+ const expires = form.expiresAt ? new Date(form.expiresAt) : null;
2401
+ const valid = (form.activityType.trim() || form.workflowKey.trim()) && form.reason.trim() && (!expires || !Number.isNaN(expires.getTime()) && expires.getTime() > Date.now());
2402
+ const create = async () => {
2403
+ if (!valid) return;
2404
+ setBusy(true);
2405
+ setError(null);
2406
+ try {
2407
+ await createPauseRule({
2408
+ activityType: form.activityType.trim() || null,
2409
+ workflowKey: form.workflowKey.trim() || null,
2410
+ reason: form.reason.trim(),
2411
+ expiresAt: expires ? expires.toISOString() : null
2412
+ });
2413
+ setForm({
2414
+ activityType: "",
2415
+ workflowKey: "",
2416
+ reason: "",
2417
+ expiresAt: ""
2418
+ });
2419
+ await rules.refetch();
2420
+ } catch (err) {
2421
+ setError(describeOperationError(err));
2422
+ } finally {
2423
+ setBusy(false);
2424
+ }
2425
+ };
2426
+ const remove = async (id) => {
2427
+ try {
2428
+ await deletePauseRule(id);
2429
+ toast.success(tr("fluidgrids.settings.pauseRules.deleted", "Rule deleted; what it paused is unpaused"));
2430
+ await rules.refetch();
2431
+ } catch (err) {
2432
+ toast.error(describeOperationError(err));
2433
+ }
2434
+ };
2435
+ return /* @__PURE__ */ jsxs(Card$1, {
2436
+ icon: "mdi:pause-octagon-outline",
2437
+ title: tr("fluidgrids.settings.pauseRules.title", "Activity pause rules"),
2438
+ description: tr("fluidgrids.settings.pauseRules.description", "A rule pauses matching durable activities when they are scheduled or retried, for example while a downstream system is down. Deleting it unpauses them."),
2439
+ testId: "settings-pause-rules",
2440
+ children: [
2441
+ /* @__PURE__ */ jsx(FieldError, { message: rules.error }),
2442
+ rules.data && rules.data.length === 0 && /* @__PURE__ */ jsx("p", {
2443
+ className: "mb-3 text-sm text-text-secondary",
2444
+ children: tr("fluidgrids.settings.pauseRules.empty", "No pause rules.")
2445
+ }),
2446
+ rules.data && rules.data.length > 0 && /* @__PURE__ */ jsx("ul", {
2447
+ className: "mb-4 divide-y divide-border-secondary rounded-md border border-border-secondary",
2448
+ children: rules.data.map((rule) => /* @__PURE__ */ jsxs("li", {
2449
+ className: "flex flex-wrap items-center gap-3 px-3 py-2 text-sm",
2450
+ "data-testid": `pause-rule-${rule.id}`,
2451
+ children: [
2452
+ /* @__PURE__ */ jsx("code", {
2453
+ className: "font-mono text-xs text-text-primary",
2454
+ children: rule.activityType ?? rule.nodeType ?? "*"
2455
+ }),
2456
+ rule.workflowKey && /* @__PURE__ */ jsx("span", {
2457
+ className: "text-xs text-text-secondary",
2458
+ children: rule.workflowKey
2459
+ }),
2460
+ /* @__PURE__ */ jsx("span", {
2461
+ className: "text-text-primary",
2462
+ children: rule.reason
2463
+ }),
2464
+ rule.expiresAt && /* @__PURE__ */ jsx("span", {
2465
+ className: "text-xs text-text-secondary",
2466
+ children: tr("fluidgrids.settings.pauseRules.until", "until {{date}}", { date: new Date(rule.expiresAt).toLocaleString() })
2467
+ }),
2468
+ canEdit && /* @__PURE__ */ jsx(Button$1, {
2469
+ size: "sm",
2470
+ variant: "ghost",
2471
+ className: "ml-auto text-[var(--color-status-error-text)]",
2472
+ onClick: () => void remove(rule.id),
2473
+ "data-testid": `pause-rule-delete-${rule.id}`,
2474
+ children: tr("fluidgrids.settings.common.delete", "Delete")
2475
+ })
2476
+ ]
2477
+ }, rule.id))
2478
+ }),
2479
+ canEdit && /* @__PURE__ */ jsxs("div", {
2480
+ className: "grid gap-3 sm:grid-cols-2",
2481
+ children: [
2482
+ /* @__PURE__ */ jsxs("div", {
2483
+ className: "space-y-1.5",
2484
+ children: [/* @__PURE__ */ jsx(Label, {
2485
+ htmlFor: "pause-rule-type",
2486
+ children: tr("fluidgrids.settings.pauseRules.activityType", "Activity or node type")
2487
+ }), /* @__PURE__ */ jsx(Input, {
2488
+ id: "pause-rule-type",
2489
+ value: form.activityType,
2490
+ placeholder: "store/crm",
2491
+ onChange: (event) => setForm({
2492
+ ...form,
2493
+ activityType: event.target.value
2494
+ }),
2495
+ "data-testid": "pause-rule-type"
2496
+ })]
2497
+ }),
2498
+ /* @__PURE__ */ jsxs("div", {
2499
+ className: "space-y-1.5",
2500
+ children: [/* @__PURE__ */ jsx(Label, {
2501
+ htmlFor: "pause-rule-workflow",
2502
+ children: tr("fluidgrids.settings.pauseRules.workflowKey", "Workflow key (optional)")
2503
+ }), /* @__PURE__ */ jsx(Input, {
2504
+ id: "pause-rule-workflow",
2505
+ value: form.workflowKey,
2506
+ onChange: (event) => setForm({
2507
+ ...form,
2508
+ workflowKey: event.target.value
2509
+ }),
2510
+ "data-testid": "pause-rule-workflow"
2511
+ })]
2512
+ }),
2513
+ /* @__PURE__ */ jsxs("div", {
2514
+ className: "space-y-1.5",
2515
+ children: [/* @__PURE__ */ jsx(Label, {
2516
+ htmlFor: "pause-rule-reason",
2517
+ children: tr("fluidgrids.settings.pauseRules.reason", "Reason")
2518
+ }), /* @__PURE__ */ jsx(Input, {
2519
+ id: "pause-rule-reason",
2520
+ value: form.reason,
2521
+ onChange: (event) => setForm({
2522
+ ...form,
2523
+ reason: event.target.value
2524
+ }),
2525
+ "data-testid": "pause-rule-reason"
2526
+ })]
2527
+ }),
2528
+ /* @__PURE__ */ jsxs("div", {
2529
+ className: "space-y-1.5",
2530
+ children: [/* @__PURE__ */ jsx(Label, {
2531
+ htmlFor: "pause-rule-expires",
2532
+ children: tr("fluidgrids.settings.pauseRules.expiresAt", "Ends at (optional)")
2533
+ }), /* @__PURE__ */ jsx(Input, {
2534
+ id: "pause-rule-expires",
2535
+ type: "datetime-local",
2536
+ value: form.expiresAt,
2537
+ onChange: (event) => setForm({
2538
+ ...form,
2539
+ expiresAt: event.target.value
2540
+ }),
2541
+ "data-testid": "pause-rule-expires"
2542
+ })]
2543
+ }),
2544
+ /* @__PURE__ */ jsxs("div", {
2545
+ className: "sm:col-span-2",
2546
+ children: [/* @__PURE__ */ jsx(FieldError, {
2547
+ message: error,
2548
+ testId: "pause-rule-error"
2549
+ }), /* @__PURE__ */ jsx(Button$1, {
2550
+ onClick: () => void create(),
2551
+ disabled: !valid || busy,
2552
+ "data-testid": "pause-rule-create",
2553
+ children: tr("fluidgrids.settings.pauseRules.create", "Add rule")
2554
+ })]
2555
+ })
2556
+ ]
2760
2557
  })
2761
2558
  ]
2762
2559
  });
2763
2560
  }
2764
-
2765
- // src/modules/settings/components/RunControlTab.tsx
2766
- /**
2767
- * Settings → Run control (BOFF-9906, Phase C): the workspace's activity pause
2768
- * rules (run control), its search attribute definitions (search attributes),
2769
- * the archive destination of finished runs (run archival) and the codec
2770
- * server the run page decodes encoded payloads with (self-hosted workers).
2771
- * Each card shows, and reads, only while its own flag is on; the tab shows
2772
- * while any of them is.
2773
- */
2774
- /**
2775
- * Whether the Run control tab has anything to show. Pause rules act on durable
2776
- * activities, so the service refuses their writes unless durable functions are
2777
- * on as well (`runControlDurable`, not `runControl` alone).
2778
- */
2779
- function hasRunControlSettings(flags) {
2780
- return flags.runControlDurable || flags.searchAttributes || flags.runArchival || flags.selfHostedWorkers;
2781
- }
2782
- function RunControlTab({ flags }) {
2783
- return /* @__PURE__ */ jsxs("div", {
2784
- className: "space-y-6",
2785
- "data-testid": "settings-run-control",
2786
- children: [
2787
- flags.runControlDurable && /* @__PURE__ */ jsx(PauseRulesCard, {}),
2788
- flags.searchAttributes && /* @__PURE__ */ jsx(SearchAttributeDefinitionsCard, {}),
2789
- flags.runArchival && /* @__PURE__ */ jsx(ArchiveSettingsCard, {}),
2790
- flags.selfHostedWorkers && /* @__PURE__ */ jsx(CodecServerCard, {})
2791
- ]
2792
- });
2793
- }
2794
- function Card$1({ icon, title, description, children, testId }) {
2795
- return /* @__PURE__ */ jsxs("section", {
2796
- className: "rounded-lg border border-border-secondary bg-background-secondary p-4 sm:p-6",
2797
- "data-testid": testId,
2561
+ const ATTRIBUTE_TYPES = [
2562
+ "KEYWORD",
2563
+ "TEXT",
2564
+ "INT",
2565
+ "DOUBLE",
2566
+ "BOOL",
2567
+ "DATETIME",
2568
+ "KEYWORD_LIST"
2569
+ ];
2570
+ /** A letter, then letters, digits and underscores (at most 64). */
2571
+ const SEARCH_ATTRIBUTE_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
2572
+ function SearchAttributeDefinitionsCard() {
2573
+ const tr = useTr();
2574
+ const definitions = useResource("search-attributes", true, fetchSearchAttributes);
2575
+ const { allowed: canEdit } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE);
2576
+ const [name, setName] = useState("");
2577
+ const [type, setType] = useState("KEYWORD");
2578
+ const [description, setDescription] = useState("");
2579
+ const [busy, setBusy] = useState(false);
2580
+ const [error, setError] = useState(null);
2581
+ const validName = SEARCH_ATTRIBUTE_NAME.test(name.trim());
2582
+ const create = async () => {
2583
+ if (!validName) return;
2584
+ setBusy(true);
2585
+ setError(null);
2586
+ try {
2587
+ await createSearchAttribute({
2588
+ name,
2589
+ type,
2590
+ description
2591
+ });
2592
+ setName("");
2593
+ setDescription("");
2594
+ await definitions.refetch();
2595
+ } catch (err) {
2596
+ setError(describeOperationError(err));
2597
+ } finally {
2598
+ setBusy(false);
2599
+ }
2600
+ };
2601
+ const remove = async (id) => {
2602
+ try {
2603
+ await deleteSearchAttribute(id);
2604
+ await definitions.refetch();
2605
+ } catch (err) {
2606
+ toast.error(describeOperationError(err));
2607
+ }
2608
+ };
2609
+ return /* @__PURE__ */ jsxs(Card$1, {
2610
+ icon: "mdi:tag-search-outline",
2611
+ title: tr("fluidgrids.settings.searchAttributes.title", "Search attributes"),
2612
+ description: tr("fluidgrids.settings.searchAttributes.description", "Typed values runs carry so the Runs list can filter on them (at most 50). Deleting one leaves the runs' values in place."),
2613
+ testId: "settings-search-attributes",
2798
2614
  children: [
2799
- /* @__PURE__ */ jsxs("div", {
2800
- className: "mb-1 flex items-center gap-2",
2801
- children: [/* @__PURE__ */ jsx(Icon, {
2802
- icon,
2803
- width: 18,
2804
- className: "text-text-secondary"
2805
- }), /* @__PURE__ */ jsx("h3", {
2806
- className: "text-base font-semibold text-text-primary",
2807
- children: title
2808
- })]
2809
- }),
2810
- /* @__PURE__ */ jsx("p", {
2811
- className: "mb-4 text-sm text-text-secondary",
2812
- children: description
2615
+ /* @__PURE__ */ jsx(FieldError, { message: definitions.error }),
2616
+ definitions.data && definitions.data.length > 0 && /* @__PURE__ */ jsx("ul", {
2617
+ className: "mb-4 divide-y divide-border-secondary rounded-md border border-border-secondary",
2618
+ children: definitions.data.map((definition) => /* @__PURE__ */ jsxs("li", {
2619
+ className: "flex flex-wrap items-center gap-3 px-3 py-2 text-sm",
2620
+ "data-testid": `search-attribute-${definition.name}`,
2621
+ children: [
2622
+ /* @__PURE__ */ jsx("code", {
2623
+ className: "font-mono text-xs text-text-primary",
2624
+ children: definition.name
2625
+ }),
2626
+ /* @__PURE__ */ jsx(StatusPill, {
2627
+ tone: "neutral",
2628
+ children: definition.type
2629
+ }),
2630
+ definition.description && /* @__PURE__ */ jsx("span", {
2631
+ className: "text-xs text-text-secondary",
2632
+ children: definition.description
2633
+ }),
2634
+ canEdit && /* @__PURE__ */ jsx(Button$1, {
2635
+ size: "sm",
2636
+ variant: "ghost",
2637
+ className: "ml-auto text-[var(--color-status-error-text)]",
2638
+ onClick: () => void remove(definition.id),
2639
+ children: tr("fluidgrids.settings.common.delete", "Delete")
2640
+ })
2641
+ ]
2642
+ }, definition.id))
2813
2643
  }),
2814
- children
2644
+ canEdit && /* @__PURE__ */ jsxs("div", {
2645
+ className: "grid gap-3 sm:grid-cols-3",
2646
+ children: [
2647
+ /* @__PURE__ */ jsxs("div", {
2648
+ className: "space-y-1.5",
2649
+ children: [/* @__PURE__ */ jsx(Label, {
2650
+ htmlFor: "search-attribute-name",
2651
+ children: tr("fluidgrids.settings.searchAttributes.name", "Name")
2652
+ }), /* @__PURE__ */ jsx(Input, {
2653
+ id: "search-attribute-name",
2654
+ value: name,
2655
+ placeholder: "customerId",
2656
+ onChange: (event) => setName(event.target.value),
2657
+ "data-testid": "search-attribute-name"
2658
+ })]
2659
+ }),
2660
+ /* @__PURE__ */ jsxs("div", {
2661
+ className: "space-y-1.5",
2662
+ children: [/* @__PURE__ */ jsx(Label, {
2663
+ htmlFor: "search-attribute-type",
2664
+ children: tr("fluidgrids.settings.searchAttributes.type", "Type")
2665
+ }), /* @__PURE__ */ jsx(NativeSelect, {
2666
+ id: "search-attribute-type",
2667
+ value: type,
2668
+ onChange: (value) => setType(ATTRIBUTE_TYPES.find((item) => item === value) ?? "KEYWORD"),
2669
+ testId: "search-attribute-type",
2670
+ children: ATTRIBUTE_TYPES.map((item) => /* @__PURE__ */ jsx("option", {
2671
+ value: item,
2672
+ children: item
2673
+ }, item))
2674
+ })]
2675
+ }),
2676
+ /* @__PURE__ */ jsxs("div", {
2677
+ className: "space-y-1.5",
2678
+ children: [/* @__PURE__ */ jsx(Label, {
2679
+ htmlFor: "search-attribute-description",
2680
+ children: tr("fluidgrids.settings.searchAttributes.descriptionField", "Description (optional)")
2681
+ }), /* @__PURE__ */ jsx(Input, {
2682
+ id: "search-attribute-description",
2683
+ value: description,
2684
+ maxLength: 1e3,
2685
+ onChange: (event) => setDescription(event.target.value)
2686
+ })]
2687
+ }),
2688
+ /* @__PURE__ */ jsxs("div", {
2689
+ className: "sm:col-span-3",
2690
+ children: [
2691
+ name && !validName && /* @__PURE__ */ jsx(FieldError, { message: tr("fluidgrids.settings.searchAttributes.badName", "Start with a letter; use letters, digits and underscores (at most 64).") }),
2692
+ /* @__PURE__ */ jsx(FieldError, {
2693
+ message: error,
2694
+ testId: "search-attribute-error"
2695
+ }),
2696
+ /* @__PURE__ */ jsx(Button$1, {
2697
+ onClick: () => void create(),
2698
+ disabled: !validName || busy,
2699
+ "data-testid": "search-attribute-create",
2700
+ children: tr("fluidgrids.settings.searchAttributes.create", "Add attribute")
2701
+ })
2702
+ ]
2703
+ })
2704
+ ]
2705
+ })
2815
2706
  ]
2816
2707
  });
2817
2708
  }
2818
- function PauseRulesCard() {
2709
+ /** An IAM role ARN in another account (the service refuses its own). */
2710
+ const ROLE_ARN = /^arn:aws[a-z-]*:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/;
2711
+ /** An S3 bucket name. */
2712
+ const BUCKET_NAME = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
2713
+ function ArchiveSettingsCard() {
2819
2714
  const tr = useTr();
2820
- const rules = useResource("pause-rules", true, fetchPauseRules);
2821
- const { allowed: canEdit } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE);
2715
+ const archive = useResource("archive-settings", true, fetchArchiveSettings);
2716
+ const { allowed: canEdit } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_DELETE);
2822
2717
  const [form, setForm] = useState({
2823
- activityType: "",
2824
- workflowKey: "",
2825
- reason: "",
2826
- expiresAt: ""
2718
+ enabled: false,
2719
+ bucket: "",
2720
+ prefix: "",
2721
+ region: "",
2722
+ roleArn: "",
2723
+ includePayloads: true
2827
2724
  });
2828
2725
  const [busy, setBusy] = useState(false);
2829
2726
  const [error, setError] = useState(null);
2830
- const expires = form.expiresAt ? new Date(form.expiresAt) : null;
2831
- const valid = (form.activityType.trim() || form.workflowKey.trim()) && form.reason.trim() && (!expires || !Number.isNaN(expires.getTime()) && expires.getTime() > Date.now());
2832
- const create = async () => {
2727
+ useEffect(() => {
2728
+ const current = archive.data;
2729
+ if (current) setForm({
2730
+ enabled: current.enabled,
2731
+ bucket: current.bucket,
2732
+ prefix: current.prefix,
2733
+ region: current.region,
2734
+ roleArn: current.roleArn,
2735
+ includePayloads: current.includePayloads
2736
+ });
2737
+ }, [archive.data]);
2738
+ const valid = BUCKET_NAME.test(form.bucket.trim()) && /^[a-z]{2}-[a-z]+-\d$/.test(form.region.trim()) && ROLE_ARN.test(form.roleArn.trim());
2739
+ const save = async () => {
2833
2740
  if (!valid) return;
2834
2741
  setBusy(true);
2835
2742
  setError(null);
2836
2743
  try {
2837
- await createPauseRule({
2838
- activityType: form.activityType.trim() || null,
2839
- workflowKey: form.workflowKey.trim() || null,
2840
- reason: form.reason.trim(),
2841
- expiresAt: expires ? expires.toISOString() : null
2842
- });
2843
- setForm({
2844
- activityType: "",
2845
- workflowKey: "",
2846
- reason: "",
2847
- expiresAt: ""
2848
- });
2849
- await rules.refetch();
2744
+ archive.set(await saveArchiveSettings({
2745
+ enabled: form.enabled,
2746
+ bucket: form.bucket.trim(),
2747
+ prefix: form.prefix.trim().replace(/^\/+|\/+$/g, ""),
2748
+ region: form.region.trim(),
2749
+ roleArn: form.roleArn.trim(),
2750
+ includePayloads: form.includePayloads
2751
+ }));
2752
+ toast.success(tr("fluidgrids.settings.archive.saved", "Archive destination saved"));
2850
2753
  } catch (err) {
2851
2754
  setError(describeOperationError(err));
2852
2755
  } finally {
2853
2756
  setBusy(false);
2854
2757
  }
2855
2758
  };
2856
- const remove = async (id) => {
2759
+ const clear = async () => {
2760
+ setBusy(true);
2761
+ setError(null);
2857
2762
  try {
2858
- await deletePauseRule(id);
2859
- toast.success(tr("fluidgrids.settings.pauseRules.deleted", "Rule deleted; what it paused is unpaused"));
2860
- await rules.refetch();
2763
+ await clearArchiveSettings();
2764
+ archive.set(null);
2765
+ setForm({
2766
+ enabled: false,
2767
+ bucket: "",
2768
+ prefix: "",
2769
+ region: "",
2770
+ roleArn: "",
2771
+ includePayloads: true
2772
+ });
2861
2773
  } catch (err) {
2862
- toast.error(describeOperationError(err));
2774
+ setError(describeOperationError(err));
2775
+ } finally {
2776
+ setBusy(false);
2863
2777
  }
2864
2778
  };
2865
2779
  return /* @__PURE__ */ jsxs(Card$1, {
2866
- icon: "mdi:pause-octagon-outline",
2867
- title: tr("fluidgrids.settings.pauseRules.title", "Activity pause rules"),
2868
- description: tr("fluidgrids.settings.pauseRules.description", "A rule pauses matching durable activities when they are scheduled or retried, for example while a downstream system is down. Deleting it unpauses them."),
2869
- testId: "settings-pause-rules",
2780
+ icon: "mdi:archive-arrow-down-outline",
2781
+ title: tr("fluidgrids.settings.archive.title", "Run archive"),
2782
+ description: tr("fluidgrids.settings.archive.description", "Before retention deletes a finished run, it is written to an S3 bucket in your AWS account. The service assumes your role with this workspace's id as the external id, and checks the destination with a probe object when you enable it."),
2783
+ testId: "settings-archive",
2870
2784
  children: [
2871
- /* @__PURE__ */ jsx(FieldError, { message: rules.error }),
2872
- rules.data && rules.data.length === 0 && /* @__PURE__ */ jsx("p", {
2873
- className: "mb-3 text-sm text-text-secondary",
2874
- children: tr("fluidgrids.settings.pauseRules.empty", "No pause rules.")
2875
- }),
2876
- rules.data && rules.data.length > 0 && /* @__PURE__ */ jsx("ul", {
2877
- className: "mb-4 divide-y divide-border-secondary rounded-md border border-border-secondary",
2878
- children: rules.data.map((rule) => /* @__PURE__ */ jsxs("li", {
2879
- className: "flex flex-wrap items-center gap-3 px-3 py-2 text-sm",
2880
- "data-testid": `pause-rule-${rule.id}`,
2881
- children: [
2882
- /* @__PURE__ */ jsx("code", {
2883
- className: "font-mono text-xs text-text-primary",
2884
- children: rule.activityType ?? rule.nodeType ?? "*"
2885
- }),
2886
- rule.workflowKey && /* @__PURE__ */ jsx("span", {
2887
- className: "text-xs text-text-secondary",
2888
- children: rule.workflowKey
2889
- }),
2890
- /* @__PURE__ */ jsx("span", {
2891
- className: "text-text-primary",
2892
- children: rule.reason
2893
- }),
2894
- rule.expiresAt && /* @__PURE__ */ jsx("span", {
2895
- className: "text-xs text-text-secondary",
2896
- children: tr("fluidgrids.settings.pauseRules.until", "until {{date}}", { date: new Date(rule.expiresAt).toLocaleString() })
2897
- }),
2898
- canEdit && /* @__PURE__ */ jsx(Button$1, {
2899
- size: "sm",
2900
- variant: "ghost",
2901
- className: "ml-auto text-[var(--color-status-error-text)]",
2902
- onClick: () => void remove(rule.id),
2903
- "data-testid": `pause-rule-delete-${rule.id}`,
2904
- children: tr("fluidgrids.settings.common.delete", "Delete")
2905
- })
2906
- ]
2907
- }, rule.id))
2785
+ archive.data && archive.data.failingRuns > 0 && /* @__PURE__ */ jsx("div", {
2786
+ role: "status",
2787
+ className: "mb-4 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)]",
2788
+ "data-testid": "archive-failing",
2789
+ children: tr("fluidgrids.settings.archive.failing", "{{count}} runs could not be archived, so their purge waits. Last error: {{error}}", {
2790
+ count: archive.data.failingRuns,
2791
+ error: archive.data.lastError ?? "—"
2792
+ })
2908
2793
  }),
2909
- canEdit && /* @__PURE__ */ jsxs("div", {
2794
+ /* @__PURE__ */ jsx(FieldError, { message: archive.error }),
2795
+ /* @__PURE__ */ jsxs("div", {
2910
2796
  className: "grid gap-3 sm:grid-cols-2",
2911
2797
  children: [
2912
2798
  /* @__PURE__ */ jsxs("div", {
2913
2799
  className: "space-y-1.5",
2914
2800
  children: [/* @__PURE__ */ jsx(Label, {
2915
- htmlFor: "pause-rule-type",
2916
- children: tr("fluidgrids.settings.pauseRules.activityType", "Activity or node type")
2801
+ htmlFor: "archive-bucket",
2802
+ children: tr("fluidgrids.settings.archive.bucket", "Bucket")
2917
2803
  }), /* @__PURE__ */ jsx(Input, {
2918
- id: "pause-rule-type",
2919
- value: form.activityType,
2920
- placeholder: "store/crm",
2804
+ id: "archive-bucket",
2805
+ value: form.bucket,
2806
+ disabled: !canEdit,
2921
2807
  onChange: (event) => setForm({
2922
2808
  ...form,
2923
- activityType: event.target.value
2809
+ bucket: event.target.value
2924
2810
  }),
2925
- "data-testid": "pause-rule-type"
2811
+ "data-testid": "archive-bucket"
2926
2812
  })]
2927
2813
  }),
2928
2814
  /* @__PURE__ */ jsxs("div", {
2929
2815
  className: "space-y-1.5",
2930
2816
  children: [/* @__PURE__ */ jsx(Label, {
2931
- htmlFor: "pause-rule-workflow",
2932
- children: tr("fluidgrids.settings.pauseRules.workflowKey", "Workflow key (optional)")
2817
+ htmlFor: "archive-prefix",
2818
+ children: tr("fluidgrids.settings.archive.prefix", "Key prefix (optional)")
2933
2819
  }), /* @__PURE__ */ jsx(Input, {
2934
- id: "pause-rule-workflow",
2935
- value: form.workflowKey,
2820
+ id: "archive-prefix",
2821
+ value: form.prefix,
2822
+ disabled: !canEdit,
2936
2823
  onChange: (event) => setForm({
2937
2824
  ...form,
2938
- workflowKey: event.target.value
2825
+ prefix: event.target.value
2939
2826
  }),
2940
- "data-testid": "pause-rule-workflow"
2827
+ "data-testid": "archive-prefix"
2941
2828
  })]
2942
2829
  }),
2943
2830
  /* @__PURE__ */ jsxs("div", {
2944
2831
  className: "space-y-1.5",
2945
2832
  children: [/* @__PURE__ */ jsx(Label, {
2946
- htmlFor: "pause-rule-reason",
2947
- children: tr("fluidgrids.settings.pauseRules.reason", "Reason")
2833
+ htmlFor: "archive-region",
2834
+ children: tr("fluidgrids.settings.archive.region", "Region")
2948
2835
  }), /* @__PURE__ */ jsx(Input, {
2949
- id: "pause-rule-reason",
2950
- value: form.reason,
2836
+ id: "archive-region",
2837
+ value: form.region,
2838
+ placeholder: "ap-south-1",
2839
+ disabled: !canEdit,
2951
2840
  onChange: (event) => setForm({
2952
2841
  ...form,
2953
- reason: event.target.value
2842
+ region: event.target.value
2954
2843
  }),
2955
- "data-testid": "pause-rule-reason"
2844
+ "data-testid": "archive-region"
2956
2845
  })]
2957
2846
  }),
2958
2847
  /* @__PURE__ */ jsxs("div", {
2959
2848
  className: "space-y-1.5",
2960
2849
  children: [/* @__PURE__ */ jsx(Label, {
2961
- htmlFor: "pause-rule-expires",
2962
- children: tr("fluidgrids.settings.pauseRules.expiresAt", "Ends at (optional)")
2850
+ htmlFor: "archive-role",
2851
+ children: tr("fluidgrids.settings.archive.roleArn", "Role ARN")
2963
2852
  }), /* @__PURE__ */ jsx(Input, {
2964
- id: "pause-rule-expires",
2965
- type: "datetime-local",
2966
- value: form.expiresAt,
2853
+ id: "archive-role",
2854
+ value: form.roleArn,
2855
+ placeholder: "arn:aws:iam::123456789012:role/fluidgrids-archive",
2856
+ disabled: !canEdit,
2967
2857
  onChange: (event) => setForm({
2968
2858
  ...form,
2969
- expiresAt: event.target.value
2859
+ roleArn: event.target.value
2970
2860
  }),
2971
- "data-testid": "pause-rule-expires"
2861
+ "data-testid": "archive-role"
2972
2862
  })]
2973
2863
  }),
2974
- /* @__PURE__ */ jsxs("div", {
2975
- className: "sm:col-span-2",
2976
- children: [/* @__PURE__ */ jsx(FieldError, {
2977
- message: error,
2978
- testId: "pause-rule-error"
2979
- }), /* @__PURE__ */ jsx(Button$1, {
2980
- onClick: () => void create(),
2981
- disabled: !valid || busy,
2982
- "data-testid": "pause-rule-create",
2983
- children: tr("fluidgrids.settings.pauseRules.create", "Add rule")
2864
+ /* @__PURE__ */ jsxs("label", {
2865
+ className: "flex items-center justify-between gap-3 text-sm sm:col-span-2",
2866
+ children: [tr("fluidgrids.settings.archive.includePayloads", "Include payloads, inputs and outputs"), /* @__PURE__ */ jsx(Switch, {
2867
+ checked: form.includePayloads,
2868
+ disabled: !canEdit,
2869
+ onCheckedChange: (includePayloads) => setForm({
2870
+ ...form,
2871
+ includePayloads
2872
+ })
2873
+ })]
2874
+ }),
2875
+ /* @__PURE__ */ jsxs("label", {
2876
+ className: "flex items-center justify-between gap-3 text-sm sm:col-span-2",
2877
+ children: [tr("fluidgrids.settings.archive.enabled", "Archive runs before they are purged"), /* @__PURE__ */ jsx(Switch, {
2878
+ checked: form.enabled,
2879
+ disabled: !canEdit,
2880
+ onCheckedChange: (enabled) => setForm({
2881
+ ...form,
2882
+ enabled
2883
+ }),
2884
+ "data-testid": "archive-enabled"
2984
2885
  })]
2985
2886
  })
2986
2887
  ]
2888
+ }),
2889
+ /* @__PURE__ */ jsx(FieldError, {
2890
+ message: error,
2891
+ testId: "archive-error"
2892
+ }),
2893
+ canEdit && /* @__PURE__ */ jsxs("div", {
2894
+ className: "mt-4 flex gap-2",
2895
+ children: [/* @__PURE__ */ jsx(Button$1, {
2896
+ onClick: () => void save(),
2897
+ disabled: !valid || busy,
2898
+ "data-testid": "archive-save",
2899
+ children: tr("fluidgrids.settings.common.save", "Save")
2900
+ }), archive.data && /* @__PURE__ */ jsx(Button$1, {
2901
+ variant: "outline",
2902
+ onClick: () => void clear(),
2903
+ disabled: busy,
2904
+ "data-testid": "archive-clear",
2905
+ children: tr("fluidgrids.settings.archive.remove", "Remove destination")
2906
+ })]
2987
2907
  })
2988
2908
  ]
2989
2909
  });
2990
2910
  }
2991
- const ATTRIBUTE_TYPES = [
2992
- "KEYWORD",
2993
- "TEXT",
2994
- "INT",
2995
- "DOUBLE",
2996
- "BOOL",
2997
- "DATETIME",
2998
- "KEYWORD_LIST"
2999
- ];
3000
- /** A letter, then letters, digits and underscores (at most 64). */
3001
- const SEARCH_ATTRIBUTE_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
3002
- function SearchAttributeDefinitionsCard() {
2911
+ function CodecServerCard() {
3003
2912
  const tr = useTr();
3004
- const definitions = useResource("search-attributes", true, fetchSearchAttributes);
3005
- const { allowed: canEdit } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE);
3006
- const [name, setName] = useState("");
3007
- const [type, setType] = useState("KEYWORD");
3008
- const [description, setDescription] = useState("");
2913
+ const settings = useResource("codec-server", true, fetchCodecServerSettings);
2914
+ const { allowed: canEdit } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_DELETE);
2915
+ const [url, setUrl] = useState("");
2916
+ const [includeCredentials, setIncludeCredentials] = useState(false);
3009
2917
  const [busy, setBusy] = useState(false);
3010
2918
  const [error, setError] = useState(null);
3011
- const validName = SEARCH_ATTRIBUTE_NAME.test(name.trim());
3012
- const create = async () => {
3013
- if (!validName) return;
3014
- setBusy(true);
3015
- setError(null);
3016
- try {
3017
- await createSearchAttribute({
3018
- name,
3019
- type,
3020
- description
3021
- });
3022
- setName("");
3023
- setDescription("");
3024
- await definitions.refetch();
2919
+ useEffect(() => {
2920
+ if (!settings.data) return;
2921
+ setUrl(settings.data.codecServerUrl ?? "");
2922
+ setIncludeCredentials(settings.data.codecServerIncludeCredentials);
2923
+ }, [settings.data]);
2924
+ const check = url.trim() ? checkCodecServerUrl(url) : null;
2925
+ const save = async () => {
2926
+ if (check && !check.ok) return;
2927
+ setBusy(true);
2928
+ setError(null);
2929
+ try {
2930
+ settings.set(await saveCodecServerSettings({
2931
+ codecServerUrl: url.trim(),
2932
+ codecServerIncludeCredentials: includeCredentials
2933
+ }));
2934
+ toast.success(tr("fluidgrids.settings.codec.saved", "Codec server saved"));
3025
2935
  } catch (err) {
3026
2936
  setError(describeOperationError(err));
3027
2937
  } finally {
3028
2938
  setBusy(false);
3029
2939
  }
3030
2940
  };
3031
- const remove = async (id) => {
3032
- try {
3033
- await deleteSearchAttribute(id);
3034
- await definitions.refetch();
3035
- } catch (err) {
3036
- toast.error(describeOperationError(err));
3037
- }
3038
- };
3039
2941
  return /* @__PURE__ */ jsxs(Card$1, {
3040
- icon: "mdi:tag-search-outline",
3041
- title: tr("fluidgrids.settings.searchAttributes.title", "Search attributes"),
3042
- description: tr("fluidgrids.settings.searchAttributes.description", "Typed values runs carry so the Runs list can filter on them (at most 50). Deleting one leaves the runs' values in place."),
3043
- testId: "settings-search-attributes",
3044
- children: [
3045
- /* @__PURE__ */ jsx(FieldError, { message: definitions.error }),
3046
- definitions.data && definitions.data.length > 0 && /* @__PURE__ */ jsx("ul", {
3047
- className: "mb-4 divide-y divide-border-secondary rounded-md border border-border-secondary",
3048
- children: definitions.data.map((definition) => /* @__PURE__ */ jsxs("li", {
3049
- className: "flex flex-wrap items-center gap-3 px-3 py-2 text-sm",
3050
- "data-testid": `search-attribute-${definition.name}`,
2942
+ icon: "mdi:lock-open-outline",
2943
+ title: tr("fluidgrids.settings.codec.title", "Codec server"),
2944
+ description: tr("fluidgrids.settings.codec.description", "Payloads your self-hosted workers encode stay encoded in FluidGrids. The run page can send them to your codec server (Temporal's codec-server protocol) to show them decoded. No FluidGrids token is ever sent there."),
2945
+ testId: "settings-codec",
2946
+ children: [/* @__PURE__ */ jsx(FieldError, { message: settings.error }), /* @__PURE__ */ jsxs("div", {
2947
+ className: "space-y-3",
2948
+ children: [
2949
+ /* @__PURE__ */ jsxs("div", {
2950
+ className: "space-y-1.5",
3051
2951
  children: [
3052
- /* @__PURE__ */ jsx("code", {
3053
- className: "font-mono text-xs text-text-primary",
3054
- children: definition.name
2952
+ /* @__PURE__ */ jsx(Label, {
2953
+ htmlFor: "codec-url",
2954
+ children: tr("fluidgrids.settings.codec.url", "Codec server URL")
3055
2955
  }),
3056
- /* @__PURE__ */ jsx(StatusPill, {
3057
- tone: "neutral",
3058
- children: definition.type
2956
+ /* @__PURE__ */ jsx(Input, {
2957
+ id: "codec-url",
2958
+ value: url,
2959
+ disabled: !canEdit,
2960
+ placeholder: "https://codec.example.com",
2961
+ onChange: (event) => setUrl(event.target.value),
2962
+ "data-testid": "codec-url"
3059
2963
  }),
3060
- definition.description && /* @__PURE__ */ jsx("span", {
3061
- className: "text-xs text-text-secondary",
3062
- children: definition.description
2964
+ check && !check.ok && /* @__PURE__ */ jsx(FieldError, {
2965
+ message: tr("fluidgrids.settings.codec.badUrl", "Use an https URL (http only for localhost)."),
2966
+ testId: "codec-url-error"
3063
2967
  }),
3064
- canEdit && /* @__PURE__ */ jsx(Button$1, {
3065
- size: "sm",
3066
- variant: "ghost",
3067
- className: "ml-auto text-[var(--color-status-error-text)]",
3068
- onClick: () => void remove(definition.id),
3069
- children: tr("fluidgrids.settings.common.delete", "Delete")
2968
+ /* @__PURE__ */ jsx("p", {
2969
+ className: "text-xs text-text-secondary",
2970
+ children: tr("fluidgrids.settings.codec.empty", "Leave it empty to show encoded payloads as encoded.")
3070
2971
  })
3071
2972
  ]
3072
- }, definition.id))
3073
- }),
3074
- canEdit && /* @__PURE__ */ jsxs("div", {
3075
- className: "grid gap-3 sm:grid-cols-3",
3076
- children: [
3077
- /* @__PURE__ */ jsxs("div", {
3078
- className: "space-y-1.5",
3079
- children: [/* @__PURE__ */ jsx(Label, {
3080
- htmlFor: "search-attribute-name",
3081
- children: tr("fluidgrids.settings.searchAttributes.name", "Name")
3082
- }), /* @__PURE__ */ jsx(Input, {
3083
- id: "search-attribute-name",
3084
- value: name,
3085
- placeholder: "customerId",
3086
- onChange: (event) => setName(event.target.value),
3087
- "data-testid": "search-attribute-name"
3088
- })]
3089
- }),
3090
- /* @__PURE__ */ jsxs("div", {
3091
- className: "space-y-1.5",
3092
- children: [/* @__PURE__ */ jsx(Label, {
3093
- htmlFor: "search-attribute-type",
3094
- children: tr("fluidgrids.settings.searchAttributes.type", "Type")
3095
- }), /* @__PURE__ */ jsx(NativeSelect, {
3096
- id: "search-attribute-type",
3097
- value: type,
3098
- onChange: (value) => setType(ATTRIBUTE_TYPES.find((item) => item === value) ?? "KEYWORD"),
3099
- testId: "search-attribute-type",
3100
- children: ATTRIBUTE_TYPES.map((item) => /* @__PURE__ */ jsx("option", {
3101
- value: item,
3102
- children: item
3103
- }, item))
3104
- })]
3105
- }),
3106
- /* @__PURE__ */ jsxs("div", {
3107
- className: "space-y-1.5",
3108
- children: [/* @__PURE__ */ jsx(Label, {
3109
- htmlFor: "search-attribute-description",
3110
- children: tr("fluidgrids.settings.searchAttributes.descriptionField", "Description (optional)")
3111
- }), /* @__PURE__ */ jsx(Input, {
3112
- id: "search-attribute-description",
3113
- value: description,
3114
- maxLength: 1e3,
3115
- onChange: (event) => setDescription(event.target.value)
3116
- })]
3117
- }),
3118
- /* @__PURE__ */ jsxs("div", {
3119
- className: "sm:col-span-3",
3120
- children: [
3121
- name && !validName && /* @__PURE__ */ jsx(FieldError, { message: tr("fluidgrids.settings.searchAttributes.badName", "Start with a letter; use letters, digits and underscores (at most 64).") }),
3122
- /* @__PURE__ */ jsx(FieldError, {
3123
- message: error,
3124
- testId: "search-attribute-error"
3125
- }),
3126
- /* @__PURE__ */ jsx(Button$1, {
3127
- onClick: () => void create(),
3128
- disabled: !validName || busy,
3129
- "data-testid": "search-attribute-create",
3130
- children: tr("fluidgrids.settings.searchAttributes.create", "Add attribute")
3131
- })
3132
- ]
3133
- })
3134
- ]
3135
- })
3136
- ]
2973
+ }),
2974
+ /* @__PURE__ */ jsxs("label", {
2975
+ className: "flex items-center justify-between gap-3 text-sm",
2976
+ children: [tr("fluidgrids.settings.codec.credentials", "Send my browser's own credentials (cookies) to it"), /* @__PURE__ */ jsx(Switch, {
2977
+ checked: includeCredentials,
2978
+ disabled: !canEdit,
2979
+ onCheckedChange: setIncludeCredentials,
2980
+ "data-testid": "codec-credentials"
2981
+ })]
2982
+ }),
2983
+ /* @__PURE__ */ jsx(FieldError, {
2984
+ message: error,
2985
+ testId: "codec-error"
2986
+ }),
2987
+ canEdit && /* @__PURE__ */ jsx(Button$1, {
2988
+ onClick: () => void save(),
2989
+ disabled: busy || check !== null && !check.ok,
2990
+ "data-testid": "codec-save",
2991
+ children: tr("fluidgrids.settings.common.save", "Save")
2992
+ })
2993
+ ]
2994
+ })]
3137
2995
  });
3138
2996
  }
3139
- /** An IAM role ARN in another account (the service refuses its own). */
3140
- const ROLE_ARN = /^arn:aws[a-z-]*:iam::\d{12}:role\/[\w+=,.@/-]{1,512}$/;
3141
- /** An S3 bucket name. */
3142
- const BUCKET_NAME = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
3143
- function ArchiveSettingsCard() {
2997
+
2998
+ // src/modules/settings/utils/error-trigger.ts
2999
+ /**
3000
+ * Error Workflow setting (BOFF-7514): the chosen workflow starts at its Error
3001
+ * Trigger node, so one without that node never starts. Mirrors the backend's
3002
+ * `findErrorTriggerNodeId` in wspace-workflow-svc.
3003
+ */
3004
+ const ERROR_TRIGGER_NODE_TYPE = "internal:error-trigger";
3005
+ function asRecord(value) {
3006
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3007
+ }
3008
+ /** Reads `data.nodes`, falling back to the legacy top-level `nodes`. */
3009
+ function hasErrorTriggerNode(definitionJson) {
3010
+ const definition = asRecord(definitionJson);
3011
+ const dataNodes = asRecord(definition?.data)?.nodes;
3012
+ const nodes = Array.isArray(dataNodes) ? dataNodes : definition?.nodes;
3013
+ return Array.isArray(nodes) && nodes.some((node) => asRecord(node)?.type === "internal:error-trigger");
3014
+ }
3015
+
3016
+ // src/modules/settings/utils/optional-positive-int.ts
3017
+ /**
3018
+ * Number inputs whose empty value means "no limit" — Max parallel executions
3019
+ * (BOFF-7647) and Timeout in seconds (BOFF-7514). Empty is also what an unset
3020
+ * value means to the engine; anything else is clamped to a whole number of at
3021
+ * least 1.
3022
+ */
3023
+ function parseOptionalPositiveInt(value) {
3024
+ if (value.trim() === "") return void 0;
3025
+ const parsed = Number(value);
3026
+ return Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : void 0;
3027
+ }
3028
+
3029
+ // src/modules/observability/types.ts
3030
+ const LOG_STREAM_EVENT_TYPES = [
3031
+ "LOG_ERROR",
3032
+ "LOG_WARN",
3033
+ "NODE_FAILED",
3034
+ "NODE_RETRIED",
3035
+ "RUN_CANCELLED",
3036
+ "RUN_COMPLETED",
3037
+ "RUN_FAILED",
3038
+ "RUN_STARTED"
3039
+ ];
3040
+
3041
+ // src/modules/observability/components/LogStreamingTab.tsx
3042
+ const DEFAULT_EVENT_FILTERS = [
3043
+ "RUN_FAILED",
3044
+ "NODE_FAILED",
3045
+ "LOG_ERROR"
3046
+ ];
3047
+ const PROVIDERS = [
3048
+ "WEBHOOK",
3049
+ "SYSLOG",
3050
+ "SENTRY"
3051
+ ];
3052
+ function emptyForm() {
3053
+ return {
3054
+ name: "",
3055
+ provider: "WEBHOOK",
3056
+ endpoint: "",
3057
+ eventFilters: DEFAULT_EVENT_FILTERS,
3058
+ isEnabled: true,
3059
+ secret: "",
3060
+ configText: "{}"
3061
+ };
3062
+ }
3063
+ function formFromDestination(destination) {
3064
+ return {
3065
+ name: destination.name,
3066
+ provider: destination.provider,
3067
+ endpoint: destination.endpoint,
3068
+ eventFilters: destination.eventFilters,
3069
+ isEnabled: destination.isEnabled,
3070
+ secret: "",
3071
+ configText: JSON.stringify(destination.config ?? {}, null, 2)
3072
+ };
3073
+ }
3074
+ function eventLabel(event) {
3075
+ return event.toLowerCase().split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
3076
+ }
3077
+ function endpointPlaceholder(provider) {
3078
+ switch (provider) {
3079
+ case "SYSLOG": return "logs.example.com:514";
3080
+ case "SENTRY": return "https://public@sentry.example/1";
3081
+ case "WEBHOOK": return "https://hooks.example.com/workflows";
3082
+ }
3083
+ }
3084
+ function parseConfig(configText) {
3085
+ try {
3086
+ const parsed = JSON.parse(configText || "{}");
3087
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3088
+ return parsed;
3089
+ } catch {
3090
+ return null;
3091
+ }
3092
+ }
3093
+ function formToInput(form) {
3094
+ const config = parseConfig(form.configText);
3095
+ if (!config || !form.name.trim() || !form.endpoint.trim()) return null;
3096
+ return {
3097
+ name: form.name.trim(),
3098
+ provider: form.provider,
3099
+ endpoint: form.endpoint.trim(),
3100
+ eventFilters: form.eventFilters,
3101
+ isEnabled: form.isEnabled,
3102
+ config,
3103
+ ...form.secret ? { secret: form.secret } : {}
3104
+ };
3105
+ }
3106
+ /**
3107
+ * Log stream destinations settings. Gated by the `fluidgrids.log-streams`
3108
+ * kill switch: when an admin turns it off for the workspace, the tab explains
3109
+ * that instead of loading destinations the backend would refuse to change.
3110
+ */
3111
+ function LogStreamingTab() {
3112
+ if (!useFluidGridsFlag(FLUIDGRIDS_FLAGS.logStreams)) return /* @__PURE__ */ jsx(LogStreamingDisabledNotice, {});
3113
+ return /* @__PURE__ */ jsx(LogStreamingSettings, {});
3114
+ }
3115
+ function LogStreamingDisabledNotice() {
3144
3116
  const tr = useTr();
3145
- const archive = useResource("archive-settings", true, fetchArchiveSettings);
3146
- const { allowed: canEdit } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_DELETE);
3147
- const [form, setForm] = useState({
3148
- enabled: false,
3149
- bucket: "",
3150
- prefix: "",
3151
- region: "",
3152
- roleArn: "",
3153
- includePayloads: true
3117
+ return /* @__PURE__ */ jsx(Card, {
3118
+ className: "bg-background-secondary border-border-secondary",
3119
+ "data-testid": "log-streaming-disabled",
3120
+ children: /* @__PURE__ */ jsxs(CardContent, {
3121
+ className: "flex items-start gap-3 p-6",
3122
+ children: [/* @__PURE__ */ jsx(Icon, {
3123
+ icon: "mdi:transit-connection-variant",
3124
+ width: 20,
3125
+ className: "mt-0.5 shrink-0 text-text-tertiary",
3126
+ "aria-hidden": "true"
3127
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
3128
+ className: "text-base font-semibold text-text-primary",
3129
+ children: tr("fluidgrids.settings.logStreaming.disabledTitle", "Log streaming is turned off")
3130
+ }), /* @__PURE__ */ jsx("p", {
3131
+ className: "mt-1 text-sm text-text-secondary",
3132
+ children: tr("fluidgrids.settings.logStreaming.disabledBody", "An administrator has turned off log streaming for this workspace. Run events are not forwarded to external destinations.")
3133
+ })] })]
3134
+ })
3154
3135
  });
3155
- const [busy, setBusy] = useState(false);
3156
- const [error, setError] = useState(null);
3136
+ }
3137
+ function LogStreamingSettings() {
3138
+ const tr = useTr();
3139
+ const { destinations, loading, error, refetch } = useLogStreamDestinations();
3140
+ const actions = useLogStreamDestinationActions();
3141
+ const [form, setForm] = useState(emptyForm);
3142
+ const [editingId, setEditingId] = useState(null);
3143
+ const [showForm, setShowForm] = useState(false);
3144
+ const editingDestination = useMemo(() => destinations.find((destination) => destination.id === editingId), [destinations, editingId]);
3157
3145
  useEffect(() => {
3158
- const current = archive.data;
3159
- if (current) setForm({
3160
- enabled: current.enabled,
3161
- bucket: current.bucket,
3162
- prefix: current.prefix,
3163
- region: current.region,
3164
- roleArn: current.roleArn,
3165
- includePayloads: current.includePayloads
3166
- });
3167
- }, [archive.data]);
3168
- const valid = BUCKET_NAME.test(form.bucket.trim()) && /^[a-z]{2}-[a-z]+-\d$/.test(form.region.trim()) && ROLE_ARN.test(form.roleArn.trim());
3169
- const save = async () => {
3170
- if (!valid) return;
3171
- setBusy(true);
3172
- setError(null);
3173
- try {
3174
- archive.set(await saveArchiveSettings({
3175
- enabled: form.enabled,
3176
- bucket: form.bucket.trim(),
3177
- prefix: form.prefix.trim().replace(/^\/+|\/+$/g, ""),
3178
- region: form.region.trim(),
3179
- roleArn: form.roleArn.trim(),
3180
- includePayloads: form.includePayloads
3181
- }));
3182
- toast.success(tr("fluidgrids.settings.archive.saved", "Archive destination saved"));
3183
- } catch (err) {
3184
- setError(describeOperationError(err));
3185
- } finally {
3186
- setBusy(false);
3146
+ if (editingDestination) setForm(formFromDestination(editingDestination));
3147
+ }, [editingDestination]);
3148
+ const resetForm = useCallback(() => {
3149
+ setForm(emptyForm());
3150
+ setEditingId(null);
3151
+ setShowForm(false);
3152
+ }, []);
3153
+ const save = useCallback(async () => {
3154
+ const input = formToInput(form);
3155
+ if (!input) {
3156
+ toast.error(tr("fluidgrids.settings.logStreaming.invalidForm", "Enter a name, endpoint, and valid JSON configuration."));
3157
+ return;
3187
3158
  }
3188
- };
3189
- const clear = async () => {
3190
- setBusy(true);
3191
- setError(null);
3192
- try {
3193
- await clearArchiveSettings();
3194
- archive.set(null);
3195
- setForm({
3196
- enabled: false,
3197
- bucket: "",
3198
- prefix: "",
3199
- region: "",
3200
- roleArn: "",
3201
- includePayloads: true
3202
- });
3203
- } catch (err) {
3204
- setError(describeOperationError(err));
3205
- } finally {
3206
- setBusy(false);
3159
+ const result = editingId ? await actions.update(editingId, input) : await actions.create(input);
3160
+ if (!result?.success) {
3161
+ toast.error(result?.message || tr("fluidgrids.settings.logStreaming.saveFailed", "Could not save log streaming destination."));
3162
+ return;
3207
3163
  }
3208
- };
3209
- return /* @__PURE__ */ jsxs(Card$1, {
3210
- icon: "mdi:archive-arrow-down-outline",
3211
- title: tr("fluidgrids.settings.archive.title", "Run archive"),
3212
- description: tr("fluidgrids.settings.archive.description", "Before retention deletes a finished run, it is written to an S3 bucket in your AWS account. The service assumes your role with this workspace's id as the external id, and checks the destination with a probe object when you enable it."),
3213
- testId: "settings-archive",
3214
- children: [
3215
- archive.data && archive.data.failingRuns > 0 && /* @__PURE__ */ jsx("div", {
3216
- role: "status",
3217
- className: "mb-4 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)]",
3218
- "data-testid": "archive-failing",
3219
- children: tr("fluidgrids.settings.archive.failing", "{{count}} runs could not be archived, so their purge waits. Last error: {{error}}", {
3220
- count: archive.data.failingRuns,
3221
- error: archive.data.lastError ?? "—"
3222
- })
3223
- }),
3224
- /* @__PURE__ */ jsx(FieldError, { message: archive.error }),
3225
- /* @__PURE__ */ jsxs("div", {
3226
- className: "grid gap-3 sm:grid-cols-2",
3227
- children: [
3228
- /* @__PURE__ */ jsxs("div", {
3229
- className: "space-y-1.5",
3230
- children: [/* @__PURE__ */ jsx(Label, {
3231
- htmlFor: "archive-bucket",
3232
- children: tr("fluidgrids.settings.archive.bucket", "Bucket")
3233
- }), /* @__PURE__ */ jsx(Input, {
3234
- id: "archive-bucket",
3235
- value: form.bucket,
3236
- disabled: !canEdit,
3237
- onChange: (event) => setForm({
3238
- ...form,
3239
- bucket: event.target.value
3240
- }),
3241
- "data-testid": "archive-bucket"
3242
- })]
3243
- }),
3244
- /* @__PURE__ */ jsxs("div", {
3245
- className: "space-y-1.5",
3246
- children: [/* @__PURE__ */ jsx(Label, {
3247
- htmlFor: "archive-prefix",
3248
- children: tr("fluidgrids.settings.archive.prefix", "Key prefix (optional)")
3249
- }), /* @__PURE__ */ jsx(Input, {
3250
- id: "archive-prefix",
3251
- value: form.prefix,
3252
- disabled: !canEdit,
3253
- onChange: (event) => setForm({
3254
- ...form,
3255
- prefix: event.target.value
3256
- }),
3257
- "data-testid": "archive-prefix"
3258
- })]
3259
- }),
3260
- /* @__PURE__ */ jsxs("div", {
3261
- className: "space-y-1.5",
3262
- children: [/* @__PURE__ */ jsx(Label, {
3263
- htmlFor: "archive-region",
3264
- children: tr("fluidgrids.settings.archive.region", "Region")
3265
- }), /* @__PURE__ */ jsx(Input, {
3266
- id: "archive-region",
3267
- value: form.region,
3268
- placeholder: "ap-south-1",
3269
- disabled: !canEdit,
3270
- onChange: (event) => setForm({
3271
- ...form,
3272
- region: event.target.value
3273
- }),
3274
- "data-testid": "archive-region"
3275
- })]
3276
- }),
3277
- /* @__PURE__ */ jsxs("div", {
3278
- className: "space-y-1.5",
3279
- children: [/* @__PURE__ */ jsx(Label, {
3280
- htmlFor: "archive-role",
3281
- children: tr("fluidgrids.settings.archive.roleArn", "Role ARN")
3282
- }), /* @__PURE__ */ jsx(Input, {
3283
- id: "archive-role",
3284
- value: form.roleArn,
3285
- placeholder: "arn:aws:iam::123456789012:role/fluidgrids-archive",
3286
- disabled: !canEdit,
3287
- onChange: (event) => setForm({
3288
- ...form,
3289
- roleArn: event.target.value
3290
- }),
3291
- "data-testid": "archive-role"
3292
- })]
3293
- }),
3294
- /* @__PURE__ */ jsxs("label", {
3295
- className: "flex items-center justify-between gap-3 text-sm sm:col-span-2",
3296
- children: [tr("fluidgrids.settings.archive.includePayloads", "Include payloads, inputs and outputs"), /* @__PURE__ */ jsx(Switch, {
3297
- checked: form.includePayloads,
3298
- disabled: !canEdit,
3299
- onCheckedChange: (includePayloads) => setForm({
3300
- ...form,
3301
- includePayloads
3302
- })
3303
- })]
3304
- }),
3305
- /* @__PURE__ */ jsxs("label", {
3306
- className: "flex items-center justify-between gap-3 text-sm sm:col-span-2",
3307
- children: [tr("fluidgrids.settings.archive.enabled", "Archive runs before they are purged"), /* @__PURE__ */ jsx(Switch, {
3308
- checked: form.enabled,
3309
- disabled: !canEdit,
3310
- onCheckedChange: (enabled) => setForm({
3311
- ...form,
3312
- enabled
3313
- }),
3314
- "data-testid": "archive-enabled"
3315
- })]
3316
- })
3317
- ]
3318
- }),
3319
- /* @__PURE__ */ jsx(FieldError, {
3320
- message: error,
3321
- testId: "archive-error"
3164
+ toast.success(tr("fluidgrids.settings.logStreaming.saved", "Log streaming destination saved."));
3165
+ resetForm();
3166
+ await refetch();
3167
+ }, [
3168
+ actions,
3169
+ editingId,
3170
+ form,
3171
+ refetch,
3172
+ resetForm,
3173
+ tr
3174
+ ]);
3175
+ const remove = useCallback(async (destination) => {
3176
+ if (!window.confirm(`Delete “${destination.name}”?`)) return;
3177
+ const result = await actions.remove(destination.id);
3178
+ if (!result?.success) {
3179
+ toast.error(result?.message || tr("fluidgrids.settings.logStreaming.deleteFailed", "Could not delete destination."));
3180
+ return;
3181
+ }
3182
+ toast.success(tr("fluidgrids.settings.logStreaming.deleted", "Log streaming destination deleted."));
3183
+ if (editingId === destination.id) resetForm();
3184
+ await refetch();
3185
+ }, [
3186
+ actions,
3187
+ editingId,
3188
+ refetch,
3189
+ resetForm,
3190
+ tr
3191
+ ]);
3192
+ const sendTest = useCallback(async (destination) => {
3193
+ const result = await actions.test(destination.id);
3194
+ if (!result) return;
3195
+ if (result.success) toast.success(tr("fluidgrids.settings.logStreaming.testSucceeded", "Test event delivered."));
3196
+ else toast.error(result.message || tr("fluidgrids.settings.logStreaming.testFailed", "Test event could not be delivered."));
3197
+ await refetch();
3198
+ }, [
3199
+ actions,
3200
+ refetch,
3201
+ tr
3202
+ ]);
3203
+ const toggleEvent = (event) => {
3204
+ setForm((current) => ({
3205
+ ...current,
3206
+ eventFilters: current.eventFilters.includes(event) ? current.eventFilters.filter((item) => item !== event) : [...current.eventFilters, event]
3207
+ }));
3208
+ };
3209
+ return /* @__PURE__ */ jsxs("div", {
3210
+ className: "space-y-6",
3211
+ children: [
3212
+ /* @__PURE__ */ jsxs("div", {
3213
+ className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between",
3214
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
3215
+ className: "text-lg font-semibold text-text-primary",
3216
+ children: tr("fluidgrids.settings.logStreaming.title", "Log streaming")
3217
+ }), /* @__PURE__ */ jsx("p", {
3218
+ className: "mt-1 max-w-2xl text-sm text-text-secondary",
3219
+ children: tr("fluidgrids.settings.logStreaming.description", "Send workflow run and node events to workspace-managed observability destinations. Secrets are write-only and never returned.")
3220
+ })] }), /* @__PURE__ */ jsxs(Button$1, {
3221
+ onClick: () => {
3222
+ setForm(emptyForm());
3223
+ setEditingId(null);
3224
+ setShowForm(true);
3225
+ },
3226
+ disabled: actions.loading,
3227
+ children: [/* @__PURE__ */ jsx(Icon, {
3228
+ icon: "mdi:plus",
3229
+ className: "mr-1",
3230
+ width: 16
3231
+ }), tr("fluidgrids.settings.logStreaming.add", "Add destination")]
3232
+ })]
3322
3233
  }),
3323
- canEdit && /* @__PURE__ */ jsxs("div", {
3324
- className: "mt-4 flex gap-2",
3325
- children: [/* @__PURE__ */ jsx(Button$1, {
3326
- onClick: () => void save(),
3327
- disabled: !valid || busy,
3328
- "data-testid": "archive-save",
3329
- children: tr("fluidgrids.settings.common.save", "Save")
3330
- }), archive.data && /* @__PURE__ */ jsx(Button$1, {
3234
+ error && /* @__PURE__ */ jsxs("div", {
3235
+ role: "alert",
3236
+ className: "rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4 text-sm text-status-error-text",
3237
+ children: [error.message, /* @__PURE__ */ jsx(Button$1, {
3331
3238
  variant: "outline",
3332
- onClick: () => void clear(),
3333
- disabled: busy,
3334
- "data-testid": "archive-clear",
3335
- children: tr("fluidgrids.settings.archive.remove", "Remove destination")
3239
+ size: "sm",
3240
+ className: "ml-3",
3241
+ onClick: () => void refetch(),
3242
+ children: tr("fluidgrids.settings.logStreaming.retry", "Retry")
3336
3243
  })]
3337
- })
3338
- ]
3339
- });
3340
- }
3341
- function CodecServerCard() {
3342
- const tr = useTr();
3343
- const settings = useResource("codec-server", true, fetchCodecServerSettings);
3344
- const { allowed: canEdit } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_DELETE);
3345
- const [url, setUrl] = useState("");
3346
- const [includeCredentials, setIncludeCredentials] = useState(false);
3347
- const [busy, setBusy] = useState(false);
3348
- const [error, setError] = useState(null);
3349
- useEffect(() => {
3350
- if (!settings.data) return;
3351
- setUrl(settings.data.codecServerUrl ?? "");
3352
- setIncludeCredentials(settings.data.codecServerIncludeCredentials);
3353
- }, [settings.data]);
3354
- const check = url.trim() ? checkCodecServerUrl(url) : null;
3355
- const save = async () => {
3356
- if (check && !check.ok) return;
3357
- setBusy(true);
3358
- setError(null);
3359
- try {
3360
- settings.set(await saveCodecServerSettings({
3361
- codecServerUrl: url.trim(),
3362
- codecServerIncludeCredentials: includeCredentials
3363
- }));
3364
- toast.success(tr("fluidgrids.settings.codec.saved", "Codec server saved"));
3365
- } catch (err) {
3366
- setError(describeOperationError(err));
3367
- } finally {
3368
- setBusy(false);
3369
- }
3370
- };
3371
- return /* @__PURE__ */ jsxs(Card$1, {
3372
- icon: "mdi:lock-open-outline",
3373
- title: tr("fluidgrids.settings.codec.title", "Codec server"),
3374
- description: tr("fluidgrids.settings.codec.description", "Payloads your self-hosted workers encode stay encoded in FluidGrids. The run page can send them to your codec server (Temporal's codec-server protocol) to show them decoded. No FluidGrids token is ever sent there."),
3375
- testId: "settings-codec",
3376
- children: [/* @__PURE__ */ jsx(FieldError, { message: settings.error }), /* @__PURE__ */ jsxs("div", {
3377
- className: "space-y-3",
3378
- children: [
3379
- /* @__PURE__ */ jsxs("div", {
3380
- className: "space-y-1.5",
3244
+ }),
3245
+ showForm && /* @__PURE__ */ jsxs(Card, {
3246
+ className: "border-border-secondary bg-background-secondary",
3247
+ children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs("div", {
3248
+ className: "flex items-center justify-between gap-3",
3249
+ children: [/* @__PURE__ */ jsx("h3", {
3250
+ className: "font-medium text-text-primary",
3251
+ children: editingId ? tr("fluidgrids.settings.logStreaming.edit", "Edit destination") : tr("fluidgrids.settings.logStreaming.new", "New destination")
3252
+ }), /* @__PURE__ */ jsx(Button$1, {
3253
+ variant: "ghost",
3254
+ size: "sm",
3255
+ onClick: resetForm,
3256
+ children: /* @__PURE__ */ jsx(Icon, {
3257
+ icon: "mdi:close",
3258
+ width: 18
3259
+ })
3260
+ })]
3261
+ }) }), /* @__PURE__ */ jsxs(CardContent, {
3262
+ className: "space-y-5",
3381
3263
  children: [
3382
- /* @__PURE__ */ jsx(Label, {
3383
- htmlFor: "codec-url",
3384
- children: tr("fluidgrids.settings.codec.url", "Codec server URL")
3264
+ /* @__PURE__ */ jsxs("div", {
3265
+ className: "grid gap-4 md:grid-cols-2",
3266
+ children: [/* @__PURE__ */ jsxs("div", {
3267
+ className: "space-y-2",
3268
+ children: [/* @__PURE__ */ jsx(Label, {
3269
+ htmlFor: "log-stream-name",
3270
+ children: tr("fluidgrids.settings.logStreaming.name", "Name")
3271
+ }), /* @__PURE__ */ jsx(Input, {
3272
+ id: "log-stream-name",
3273
+ value: form.name,
3274
+ onChange: (event) => setForm((current) => ({
3275
+ ...current,
3276
+ name: event.target.value
3277
+ })),
3278
+ placeholder: "Production error stream"
3279
+ })]
3280
+ }), /* @__PURE__ */ jsxs("div", {
3281
+ className: "space-y-2",
3282
+ children: [/* @__PURE__ */ jsx(Label, {
3283
+ htmlFor: "log-stream-provider",
3284
+ children: tr("fluidgrids.settings.logStreaming.provider", "Provider")
3285
+ }), /* @__PURE__ */ jsx("select", {
3286
+ id: "log-stream-provider",
3287
+ value: form.provider,
3288
+ onChange: (event) => setForm((current) => ({
3289
+ ...current,
3290
+ provider: event.target.value,
3291
+ endpoint: ""
3292
+ })),
3293
+ className: "w-full rounded-md border border-border-secondary bg-background-primary px-3 py-2 text-sm text-text-primary",
3294
+ children: PROVIDERS.map((provider) => /* @__PURE__ */ jsx("option", {
3295
+ value: provider,
3296
+ children: provider
3297
+ }, provider))
3298
+ })]
3299
+ })]
3385
3300
  }),
3386
- /* @__PURE__ */ jsx(Input, {
3387
- id: "codec-url",
3388
- value: url,
3389
- disabled: !canEdit,
3390
- placeholder: "https://codec.example.com",
3391
- onChange: (event) => setUrl(event.target.value),
3392
- "data-testid": "codec-url"
3301
+ /* @__PURE__ */ jsxs("div", {
3302
+ className: "space-y-2",
3303
+ children: [/* @__PURE__ */ jsx(Label, {
3304
+ htmlFor: "log-stream-endpoint",
3305
+ children: tr("fluidgrids.settings.logStreaming.endpoint", "Endpoint")
3306
+ }), /* @__PURE__ */ jsx(Input, {
3307
+ id: "log-stream-endpoint",
3308
+ value: form.endpoint,
3309
+ onChange: (event) => setForm((current) => ({
3310
+ ...current,
3311
+ endpoint: event.target.value
3312
+ })),
3313
+ placeholder: endpointPlaceholder(form.provider),
3314
+ type: form.provider === "WEBHOOK" || form.provider === "SENTRY" ? "url" : "text"
3315
+ })]
3393
3316
  }),
3394
- check && !check.ok && /* @__PURE__ */ jsx(FieldError, {
3395
- message: tr("fluidgrids.settings.codec.badUrl", "Use an https URL (http only for localhost)."),
3396
- testId: "codec-url-error"
3317
+ form.provider === "WEBHOOK" && /* @__PURE__ */ jsxs("div", {
3318
+ className: "space-y-2",
3319
+ children: [/* @__PURE__ */ jsx(Label, {
3320
+ htmlFor: "log-stream-secret",
3321
+ children: tr("fluidgrids.settings.logStreaming.secret", "Signing secret")
3322
+ }), /* @__PURE__ */ jsx(Input, {
3323
+ id: "log-stream-secret",
3324
+ value: form.secret,
3325
+ onChange: (event) => setForm((current) => ({
3326
+ ...current,
3327
+ secret: event.target.value
3328
+ })),
3329
+ placeholder: editingId ? "Leave blank to keep the current secret" : "Enter a signing secret",
3330
+ type: "password",
3331
+ autoComplete: "new-password"
3332
+ })]
3333
+ }),
3334
+ /* @__PURE__ */ jsxs("div", {
3335
+ className: "space-y-2",
3336
+ children: [/* @__PURE__ */ jsx(Label, {
3337
+ htmlFor: "log-stream-config",
3338
+ children: tr("fluidgrids.settings.logStreaming.config", "Provider configuration (JSON)")
3339
+ }), /* @__PURE__ */ jsx("textarea", {
3340
+ id: "log-stream-config",
3341
+ value: form.configText,
3342
+ onChange: (event) => setForm((current) => ({
3343
+ ...current,
3344
+ configText: event.target.value
3345
+ })),
3346
+ className: "min-h-24 w-full rounded-md border border-border-secondary bg-background-primary px-3 py-2 font-mono text-xs text-text-primary",
3347
+ spellCheck: false
3348
+ })]
3349
+ }),
3350
+ /* @__PURE__ */ jsxs("fieldset", {
3351
+ className: "space-y-2",
3352
+ children: [/* @__PURE__ */ jsx("legend", {
3353
+ className: "text-sm font-medium text-text-primary",
3354
+ children: tr("fluidgrids.settings.logStreaming.events", "Events to stream")
3355
+ }), /* @__PURE__ */ jsx("div", {
3356
+ className: "grid gap-2 sm:grid-cols-2 lg:grid-cols-4",
3357
+ children: LOG_STREAM_EVENT_TYPES.map((event) => /* @__PURE__ */ jsxs("label", {
3358
+ className: "flex items-center gap-2 text-sm text-text-secondary",
3359
+ children: [/* @__PURE__ */ jsx("input", {
3360
+ type: "checkbox",
3361
+ checked: form.eventFilters.includes(event),
3362
+ onChange: () => toggleEvent(event),
3363
+ className: "size-4 rounded border-border-secondary accent-action-primary-bg"
3364
+ }), eventLabel(event)]
3365
+ }, event))
3366
+ })]
3367
+ }),
3368
+ /* @__PURE__ */ jsxs("label", {
3369
+ className: "flex items-center gap-2 text-sm text-text-secondary",
3370
+ children: [/* @__PURE__ */ jsx("input", {
3371
+ type: "checkbox",
3372
+ checked: form.isEnabled,
3373
+ onChange: (event) => setForm((current) => ({
3374
+ ...current,
3375
+ isEnabled: event.target.checked
3376
+ })),
3377
+ className: "size-4 rounded border-border-secondary accent-action-primary-bg"
3378
+ }), tr("fluidgrids.settings.logStreaming.enabled", "Destination enabled")]
3397
3379
  }),
3398
- /* @__PURE__ */ jsx("p", {
3399
- className: "text-xs text-text-secondary",
3400
- children: tr("fluidgrids.settings.codec.empty", "Leave it empty to show encoded payloads as encoded.")
3380
+ /* @__PURE__ */ jsxs("div", {
3381
+ className: "flex justify-end gap-2",
3382
+ children: [/* @__PURE__ */ jsx(Button$1, {
3383
+ variant: "outline",
3384
+ onClick: resetForm,
3385
+ children: tr("fluidgrids.settings.logStreaming.cancel", "Cancel")
3386
+ }), /* @__PURE__ */ jsx(Button$1, {
3387
+ "data-agent": "confirm",
3388
+ onClick: () => void save(),
3389
+ disabled: actions.loading,
3390
+ children: actions.loading ? tr("fluidgrids.settings.logStreaming.saving", "Saving…") : tr("fluidgrids.settings.logStreaming.save", "Save destination")
3391
+ })]
3401
3392
  })
3402
3393
  ]
3403
- }),
3404
- /* @__PURE__ */ jsxs("label", {
3405
- className: "flex items-center justify-between gap-3 text-sm",
3406
- children: [tr("fluidgrids.settings.codec.credentials", "Send my browser's own credentials (cookies) to it"), /* @__PURE__ */ jsx(Switch, {
3407
- checked: includeCredentials,
3408
- disabled: !canEdit,
3409
- onCheckedChange: setIncludeCredentials,
3410
- "data-testid": "codec-credentials"
3411
- })]
3412
- }),
3413
- /* @__PURE__ */ jsx(FieldError, {
3414
- message: error,
3415
- testId: "codec-error"
3416
- }),
3417
- canEdit && /* @__PURE__ */ jsx(Button$1, {
3418
- onClick: () => void save(),
3419
- disabled: busy || check !== null && !check.ok,
3420
- "data-testid": "codec-save",
3421
- children: tr("fluidgrids.settings.common.save", "Save")
3422
- })
3423
- ]
3424
- })]
3425
- });
3426
- }
3427
-
3428
- // src/modules/settings/utils/error-trigger.ts
3429
- /**
3430
- * Error Workflow setting (BOFF-7514): the chosen workflow starts at its Error
3431
- * Trigger node, so one without that node never starts. Mirrors the backend's
3432
- * `findErrorTriggerNodeId` in wspace-workflow-svc.
3433
- */
3434
- const ERROR_TRIGGER_NODE_TYPE = "internal:error-trigger";
3435
- function asRecord(value) {
3436
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3437
- }
3438
- /** Reads `data.nodes`, falling back to the legacy top-level `nodes`. */
3439
- function hasErrorTriggerNode(definitionJson) {
3440
- const definition = asRecord(definitionJson);
3441
- const dataNodes = asRecord(definition?.data)?.nodes;
3442
- const nodes = Array.isArray(dataNodes) ? dataNodes : definition?.nodes;
3443
- return Array.isArray(nodes) && nodes.some((node) => asRecord(node)?.type === "internal:error-trigger");
3444
- }
3445
-
3446
- // src/modules/settings/utils/optional-positive-int.ts
3447
- /**
3448
- * Number inputs whose empty value means "no limit" — Max parallel executions
3449
- * (BOFF-7647) and Timeout in seconds (BOFF-7514). Empty is also what an unset
3450
- * value means to the engine; anything else is clamped to a whole number of at
3451
- * least 1.
3452
- */
3453
- function parseOptionalPositiveInt(value) {
3454
- if (value.trim() === "") return void 0;
3455
- const parsed = Number(value);
3456
- return Number.isFinite(parsed) ? Math.max(1, Math.trunc(parsed)) : void 0;
3457
- }
3458
-
3459
- // src/modules/observability/types.ts
3460
- const LOG_STREAM_EVENT_TYPES = [
3461
- "LOG_ERROR",
3462
- "LOG_WARN",
3463
- "NODE_FAILED",
3464
- "NODE_RETRIED",
3465
- "RUN_CANCELLED",
3466
- "RUN_COMPLETED",
3467
- "RUN_FAILED",
3468
- "RUN_STARTED"
3469
- ];
3470
-
3471
- // src/modules/observability/components/LogStreamingTab.tsx
3472
- const DEFAULT_EVENT_FILTERS = [
3473
- "RUN_FAILED",
3474
- "NODE_FAILED",
3475
- "LOG_ERROR"
3476
- ];
3477
- const PROVIDERS = [
3478
- "WEBHOOK",
3479
- "SYSLOG",
3480
- "SENTRY"
3481
- ];
3482
- function emptyForm() {
3483
- return {
3484
- name: "",
3485
- provider: "WEBHOOK",
3486
- endpoint: "",
3487
- eventFilters: DEFAULT_EVENT_FILTERS,
3488
- isEnabled: true,
3489
- secret: "",
3490
- configText: "{}"
3491
- };
3492
- }
3493
- function formFromDestination(destination) {
3494
- return {
3495
- name: destination.name,
3496
- provider: destination.provider,
3497
- endpoint: destination.endpoint,
3498
- eventFilters: destination.eventFilters,
3499
- isEnabled: destination.isEnabled,
3500
- secret: "",
3501
- configText: JSON.stringify(destination.config ?? {}, null, 2)
3502
- };
3503
- }
3504
- function eventLabel(event) {
3505
- return event.toLowerCase().split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
3506
- }
3507
- function endpointPlaceholder(provider) {
3508
- switch (provider) {
3509
- case "SYSLOG": return "logs.example.com:514";
3510
- case "SENTRY": return "https://public@sentry.example/1";
3511
- case "WEBHOOK": return "https://hooks.example.com/workflows";
3512
- }
3513
- }
3514
- function parseConfig(configText) {
3515
- try {
3516
- const parsed = JSON.parse(configText || "{}");
3517
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3518
- return parsed;
3519
- } catch {
3520
- return null;
3521
- }
3522
- }
3523
- function formToInput(form) {
3524
- const config = parseConfig(form.configText);
3525
- if (!config || !form.name.trim() || !form.endpoint.trim()) return null;
3526
- return {
3527
- name: form.name.trim(),
3528
- provider: form.provider,
3529
- endpoint: form.endpoint.trim(),
3530
- eventFilters: form.eventFilters,
3531
- isEnabled: form.isEnabled,
3532
- config,
3533
- ...form.secret ? { secret: form.secret } : {}
3534
- };
3394
+ })]
3395
+ }),
3396
+ /* @__PURE__ */ jsxs("div", {
3397
+ className: "space-y-3",
3398
+ children: [
3399
+ loading && destinations.length === 0 && /* @__PURE__ */ jsx("div", {
3400
+ className: "rounded-lg border border-border-secondary bg-background-secondary p-6 text-sm text-text-secondary",
3401
+ children: tr("fluidgrids.settings.logStreaming.loading", "Loading destinations…")
3402
+ }),
3403
+ !loading && destinations.length === 0 && /* @__PURE__ */ jsxs("div", {
3404
+ className: "rounded-lg border border-dashed border-border-secondary bg-background-secondary p-8 text-center",
3405
+ children: [
3406
+ /* @__PURE__ */ jsx(Icon, {
3407
+ icon: "mdi:transit-connection-variant",
3408
+ className: "mx-auto mb-3 text-text-secondary",
3409
+ width: 32
3410
+ }),
3411
+ /* @__PURE__ */ jsx("p", {
3412
+ className: "font-medium text-text-primary",
3413
+ children: tr("fluidgrids.settings.logStreaming.emptyTitle", "No log destinations yet")
3414
+ }),
3415
+ /* @__PURE__ */ jsx("p", {
3416
+ className: "mt-1 text-sm text-text-secondary",
3417
+ children: tr("fluidgrids.settings.logStreaming.emptyDescription", "Add a webhook, syslog target, or Sentry DSN to start streaming events.")
3418
+ })
3419
+ ]
3420
+ }),
3421
+ destinations.map((destination) => /* @__PURE__ */ jsx(Card, {
3422
+ className: "border-border-secondary bg-background-secondary",
3423
+ children: /* @__PURE__ */ jsxs(CardContent, {
3424
+ className: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between",
3425
+ children: [/* @__PURE__ */ jsxs("div", {
3426
+ className: "min-w-0",
3427
+ children: [
3428
+ /* @__PURE__ */ jsxs("div", {
3429
+ className: "flex flex-wrap items-center gap-2",
3430
+ children: [
3431
+ /* @__PURE__ */ jsx("h3", {
3432
+ className: "font-medium text-text-primary",
3433
+ children: destination.name
3434
+ }),
3435
+ /* @__PURE__ */ jsx("span", {
3436
+ className: "rounded-full bg-background-tertiary px-2 py-0.5 text-xs text-text-secondary",
3437
+ children: destination.provider
3438
+ }),
3439
+ /* @__PURE__ */ jsx("span", {
3440
+ className: destination.isEnabled ? "text-xs text-status-success-text" : "text-xs text-text-secondary",
3441
+ children: destination.isEnabled ? tr("fluidgrids.settings.logStreaming.enabledBadge", "Enabled") : tr("fluidgrids.settings.logStreaming.disabledBadge", "Disabled")
3442
+ })
3443
+ ]
3444
+ }),
3445
+ /* @__PURE__ */ jsx("p", {
3446
+ className: "mt-1 truncate text-sm text-text-secondary",
3447
+ children: destination.endpoint
3448
+ }),
3449
+ /* @__PURE__ */ jsxs("p", {
3450
+ className: "mt-1 text-xs text-text-secondary",
3451
+ children: [destination.eventFilters.length > 0 ? destination.eventFilters.map(eventLabel).join(", ") : tr("fluidgrids.settings.logStreaming.allEvents", "All events"), destination.hasSecret && " · Secret configured"]
3452
+ }),
3453
+ destination.lastDispatchError && /* @__PURE__ */ jsx("p", {
3454
+ className: "mt-2 text-xs text-status-error-text",
3455
+ children: destination.lastDispatchError
3456
+ })
3457
+ ]
3458
+ }), /* @__PURE__ */ jsxs("div", {
3459
+ className: "flex shrink-0 flex-wrap gap-2",
3460
+ children: [
3461
+ /* @__PURE__ */ jsxs(Button$1, {
3462
+ variant: "outline",
3463
+ size: "sm",
3464
+ onClick: () => void sendTest(destination),
3465
+ disabled: actions.loading,
3466
+ children: [/* @__PURE__ */ jsx(Icon, {
3467
+ icon: "mdi:send-check-outline",
3468
+ className: "mr-1",
3469
+ width: 15
3470
+ }), tr("fluidgrids.settings.logStreaming.test", "Send test")]
3471
+ }),
3472
+ /* @__PURE__ */ jsx(Button$1, {
3473
+ variant: "outline",
3474
+ size: "sm",
3475
+ onClick: () => {
3476
+ setEditingId(destination.id);
3477
+ setShowForm(true);
3478
+ },
3479
+ disabled: actions.loading,
3480
+ children: tr("fluidgrids.settings.logStreaming.editButton", "Edit")
3481
+ }),
3482
+ /* @__PURE__ */ jsxs(Button$1, {
3483
+ variant: "ghost",
3484
+ size: "sm",
3485
+ onClick: () => void remove(destination),
3486
+ disabled: actions.loading,
3487
+ children: [/* @__PURE__ */ jsx(Icon, {
3488
+ icon: "mdi:trash-can-outline",
3489
+ className: "text-status-error-text",
3490
+ width: 17
3491
+ }), /* @__PURE__ */ jsx("span", {
3492
+ className: "sr-only",
3493
+ children: tr("fluidgrids.settings.logStreaming.delete", "Delete")
3494
+ })]
3495
+ })
3496
+ ]
3497
+ })]
3498
+ })
3499
+ }, destination.id))
3500
+ ]
3501
+ })
3502
+ ]
3503
+ });
3535
3504
  }
3505
+
3506
+ // src/modules/agent-definitions/components/AgentsTab.tsx
3536
3507
  /**
3537
- * Log stream destinations settings. Gated by the `fluidgrids.log-streams`
3538
- * kill switch: when an admin turns it off for the workspace, the tab explains
3539
- * that instead of loading destinations the backend would refuse to change.
3508
+ * Settings → Agents (BOFF-9962): FluidGrids agents with their own identity.
3509
+ *
3510
+ * Builders create an agent, choose the tools it may call (node type and operations) and the
3511
+ * connections those tools may use, and can require approval for every tool call. Each person
3512
+ * lets the agent act for them with a time-boxed grant (30 days by default, 365 at most) and can
3513
+ * withdraw it at any time; admins can revoke anyone's grant. An AI Agent node that names the agent
3514
+ * runs its tools as "agent for user", and every call is recorded under both identities. Giving an
3515
+ * agent more access withdraws other people's grants until they grant again.
3540
3516
  */
3541
- function LogStreamingTab() {
3542
- if (!useFluidGridsFlag(FLUIDGRIDS_FLAGS.logStreams)) return /* @__PURE__ */ jsx(LogStreamingDisabledNotice, {});
3543
- return /* @__PURE__ */ jsx(LogStreamingSettings, {});
3544
- }
3545
- function LogStreamingDisabledNotice() {
3546
- const tr = useTr();
3547
- return /* @__PURE__ */ jsx(Card, {
3548
- className: "bg-background-secondary border-border-secondary",
3549
- "data-testid": "log-streaming-disabled",
3550
- children: /* @__PURE__ */ jsxs(CardContent, {
3551
- className: "flex items-start gap-3 p-6",
3552
- children: [/* @__PURE__ */ jsx(Icon, {
3553
- icon: "mdi:transit-connection-variant",
3554
- width: 20,
3555
- className: "mt-0.5 shrink-0 text-text-tertiary",
3556
- "aria-hidden": "true"
3557
- }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
3558
- className: "text-base font-semibold text-text-primary",
3559
- children: tr("fluidgrids.settings.logStreaming.disabledTitle", "Log streaming is turned off")
3560
- }), /* @__PURE__ */ jsx("p", {
3561
- className: "mt-1 text-sm text-text-secondary",
3562
- children: tr("fluidgrids.settings.logStreaming.disabledBody", "An administrator has turned off log streaming for this workspace. Run events are not forwarded to external destinations.")
3563
- })] })]
3564
- })
3565
- });
3517
+ function errorMessage$1(err) {
3518
+ if (err instanceof AgentFormError) return err.message;
3519
+ return describeOperationError(err);
3566
3520
  }
3567
- function LogStreamingSettings() {
3521
+ function AgentEditor({ initial, onCancel, onSave }) {
3568
3522
  const tr = useTr();
3569
- const { destinations, loading, error, refetch } = useLogStreamDestinations();
3570
- const actions = useLogStreamDestinationActions();
3571
- const [form, setForm] = useState(emptyForm);
3572
- const [editingId, setEditingId] = useState(null);
3573
- const [showForm, setShowForm] = useState(false);
3574
- const editingDestination = useMemo(() => destinations.find((destination) => destination.id === editingId), [destinations, editingId]);
3575
- useEffect(() => {
3576
- if (editingDestination) setForm(formFromDestination(editingDestination));
3577
- }, [editingDestination]);
3578
- const resetForm = useCallback(() => {
3579
- setForm(emptyForm());
3580
- setEditingId(null);
3581
- setShowForm(false);
3582
- }, []);
3583
- const save = useCallback(async () => {
3584
- const input = formToInput(form);
3585
- if (!input) {
3586
- toast.error(tr("fluidgrids.settings.logStreaming.invalidForm", "Enter a name, endpoint, and valid JSON configuration."));
3587
- return;
3588
- }
3589
- const result = editingId ? await actions.update(editingId, input) : await actions.create(input);
3590
- if (!result?.success) {
3591
- toast.error(result?.message || tr("fluidgrids.settings.logStreaming.saveFailed", "Could not save log streaming destination."));
3592
- return;
3593
- }
3594
- toast.success(tr("fluidgrids.settings.logStreaming.saved", "Log streaming destination saved."));
3595
- resetForm();
3596
- await refetch();
3597
- }, [
3598
- actions,
3599
- editingId,
3600
- form,
3601
- refetch,
3602
- resetForm,
3603
- tr
3604
- ]);
3605
- const remove = useCallback(async (destination) => {
3606
- if (!window.confirm(`Delete “${destination.name}”?`)) return;
3607
- const result = await actions.remove(destination.id);
3608
- if (!result?.success) {
3609
- toast.error(result?.message || tr("fluidgrids.settings.logStreaming.deleteFailed", "Could not delete destination."));
3610
- return;
3523
+ const [form, setForm] = useState(initial);
3524
+ const [busy, setBusy] = useState(false);
3525
+ const { connections, loading: connectionsLoading } = useConnections();
3526
+ const setTool = (index, patch) => setForm((current) => ({
3527
+ ...current,
3528
+ tools: current.tools.map((row, i) => i === index ? {
3529
+ ...row,
3530
+ ...patch
3531
+ } : row)
3532
+ }));
3533
+ const toggleConnection = (id, on) => setForm((current) => ({
3534
+ ...current,
3535
+ connectionScope: on ? [.../* @__PURE__ */ new Set([...current.connectionScope, id])] : current.connectionScope.filter((value) => value !== id)
3536
+ }));
3537
+ const submit = async () => {
3538
+ setBusy(true);
3539
+ try {
3540
+ await onSave(form);
3541
+ } catch (err) {
3542
+ toast.error(errorMessage$1(err));
3543
+ } finally {
3544
+ setBusy(false);
3611
3545
  }
3612
- toast.success(tr("fluidgrids.settings.logStreaming.deleted", "Log streaming destination deleted."));
3613
- if (editingId === destination.id) resetForm();
3614
- await refetch();
3615
- }, [
3616
- actions,
3617
- editingId,
3618
- refetch,
3619
- resetForm,
3620
- tr
3621
- ]);
3622
- const sendTest = useCallback(async (destination) => {
3623
- const result = await actions.test(destination.id);
3624
- if (!result) return;
3625
- if (result.success) toast.success(tr("fluidgrids.settings.logStreaming.testSucceeded", "Test event delivered."));
3626
- else toast.error(result.message || tr("fluidgrids.settings.logStreaming.testFailed", "Test event could not be delivered."));
3627
- await refetch();
3628
- }, [
3629
- actions,
3630
- refetch,
3631
- tr
3632
- ]);
3633
- const toggleEvent = (event) => {
3634
- setForm((current) => ({
3635
- ...current,
3636
- eventFilters: current.eventFilters.includes(event) ? current.eventFilters.filter((item) => item !== event) : [...current.eventFilters, event]
3637
- }));
3638
3546
  };
3639
3547
  return /* @__PURE__ */ jsxs("div", {
3640
- className: "space-y-6",
3548
+ className: "rounded-lg border border-border-secondary bg-background-primary p-4 space-y-4",
3549
+ "data-testid": "agent-definition-editor",
3641
3550
  children: [
3642
3551
  /* @__PURE__ */ jsxs("div", {
3643
- className: "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between",
3644
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
3645
- className: "text-lg font-semibold text-text-primary",
3646
- children: tr("fluidgrids.settings.logStreaming.title", "Log streaming")
3647
- }), /* @__PURE__ */ jsx("p", {
3648
- className: "mt-1 max-w-2xl text-sm text-text-secondary",
3649
- children: tr("fluidgrids.settings.logStreaming.description", "Send workflow run and node events to workspace-managed observability destinations. Secrets are write-only and never returned.")
3650
- })] }), /* @__PURE__ */ jsxs(Button$1, {
3651
- onClick: () => {
3652
- setForm(emptyForm());
3653
- setEditingId(null);
3654
- setShowForm(true);
3655
- },
3656
- disabled: actions.loading,
3657
- children: [/* @__PURE__ */ jsx(Icon, {
3658
- icon: "mdi:plus",
3659
- className: "mr-1",
3660
- width: 16
3661
- }), tr("fluidgrids.settings.logStreaming.add", "Add destination")]
3552
+ className: "grid gap-3 sm:grid-cols-2",
3553
+ children: [/* @__PURE__ */ jsxs("div", {
3554
+ className: "space-y-1",
3555
+ children: [/* @__PURE__ */ jsx(Label, {
3556
+ htmlFor: "agent-name",
3557
+ children: tr("fluidgrids.agents.name", "Name")
3558
+ }), /* @__PURE__ */ jsx(Input, {
3559
+ id: "agent-name",
3560
+ value: form.name,
3561
+ onChange: (e) => setForm({
3562
+ ...form,
3563
+ name: e.target.value
3564
+ }),
3565
+ "data-testid": "agent-definition-name"
3566
+ })]
3567
+ }), /* @__PURE__ */ jsxs("div", {
3568
+ className: "flex items-end gap-2",
3569
+ children: [/* @__PURE__ */ jsx(Switch, {
3570
+ id: "agent-active",
3571
+ checked: form.status === "ACTIVE",
3572
+ onCheckedChange: (on) => setForm({
3573
+ ...form,
3574
+ status: on ? "ACTIVE" : "DISABLED"
3575
+ })
3576
+ }), /* @__PURE__ */ jsx(Label, {
3577
+ htmlFor: "agent-active",
3578
+ children: tr("fluidgrids.agents.active", "Active")
3579
+ })]
3580
+ })]
3581
+ }),
3582
+ /* @__PURE__ */ jsxs("div", {
3583
+ className: "space-y-1",
3584
+ children: [/* @__PURE__ */ jsx(Label, {
3585
+ htmlFor: "agent-description",
3586
+ children: tr("fluidgrids.agents.description", "Description")
3587
+ }), /* @__PURE__ */ jsx(Textarea, {
3588
+ id: "agent-description",
3589
+ value: form.description,
3590
+ onChange: (e) => setForm({
3591
+ ...form,
3592
+ description: e.target.value
3593
+ }),
3594
+ rows: 2
3662
3595
  })]
3663
3596
  }),
3664
- error && /* @__PURE__ */ jsxs("div", {
3665
- role: "alert",
3666
- className: "rounded-lg border border-status-error-border bg-status-error-bg-subtle p-4 text-sm text-status-error-text",
3667
- children: [error.message, /* @__PURE__ */ jsx(Button$1, {
3668
- variant: "outline",
3669
- size: "sm",
3670
- className: "ml-3",
3671
- onClick: () => void refetch(),
3672
- children: tr("fluidgrids.settings.logStreaming.retry", "Retry")
3597
+ /* @__PURE__ */ jsxs("div", {
3598
+ className: "space-y-2",
3599
+ children: [
3600
+ /* @__PURE__ */ jsx(Label, { children: tr("fluidgrids.agents.tools", "Tools the agent may call") }),
3601
+ /* @__PURE__ */ jsx("p", {
3602
+ className: "text-xs text-text-secondary",
3603
+ children: tr("fluidgrids.agents.toolsHelp", "A node type (for example store/slack) and the operations of it the agent may call, separated by commas. Leave the operations empty to allow every operation of that node type. Nothing else can be called.")
3604
+ }),
3605
+ form.tools.map((row, index) => /* @__PURE__ */ jsxs("div", {
3606
+ className: "flex flex-wrap items-center gap-2",
3607
+ "data-testid": `agent-definition-tool-${index}`,
3608
+ children: [
3609
+ /* @__PURE__ */ jsx(Input, {
3610
+ className: "w-48",
3611
+ placeholder: "store/slack",
3612
+ value: row.nodeType,
3613
+ onChange: (e) => setTool(index, { nodeType: e.target.value }),
3614
+ "aria-label": tr("fluidgrids.agents.nodeType", "Node type")
3615
+ }),
3616
+ /* @__PURE__ */ jsx(Input, {
3617
+ className: "min-w-48 flex-1",
3618
+ placeholder: "postMessage, listChannels",
3619
+ value: row.operations,
3620
+ onChange: (e) => setTool(index, { operations: e.target.value }),
3621
+ "aria-label": tr("fluidgrids.agents.operations", "Operations")
3622
+ }),
3623
+ /* @__PURE__ */ jsx(Button$1, {
3624
+ variant: "ghost",
3625
+ size: "sm",
3626
+ onClick: () => setForm({
3627
+ ...form,
3628
+ tools: form.tools.filter((_, i) => i !== index)
3629
+ }),
3630
+ "aria-label": tr("fluidgrids.agents.removeTool", "Remove tool"),
3631
+ children: /* @__PURE__ */ jsx(Icon, {
3632
+ icon: "mdi:close",
3633
+ width: 16
3634
+ })
3635
+ })
3636
+ ]
3637
+ }, index)),
3638
+ /* @__PURE__ */ jsxs(Button$1, {
3639
+ variant: "outline",
3640
+ size: "sm",
3641
+ onClick: () => setForm({
3642
+ ...form,
3643
+ tools: [...form.tools, {
3644
+ nodeType: "",
3645
+ operations: ""
3646
+ }]
3647
+ }),
3648
+ children: [/* @__PURE__ */ jsx(Icon, {
3649
+ icon: "mdi:plus",
3650
+ width: 16
3651
+ }), tr("fluidgrids.agents.addTool", "Add tool")]
3652
+ })
3653
+ ]
3654
+ }),
3655
+ /* @__PURE__ */ jsxs("div", {
3656
+ className: "space-y-2",
3657
+ children: [/* @__PURE__ */ jsx(Label, { children: tr("fluidgrids.agents.connections", "Connections the agent's tools may use") }), connectionsLoading ? /* @__PURE__ */ jsx("p", {
3658
+ className: "text-xs text-text-secondary",
3659
+ children: tr("fluidgrids.agents.loadingConnections", "Loading connections…")
3660
+ }) : connections.length === 0 ? /* @__PURE__ */ jsx("p", {
3661
+ className: "text-xs text-text-secondary",
3662
+ children: tr("fluidgrids.agents.noConnections", "This workspace has no connections yet.")
3663
+ }) : /* @__PURE__ */ jsx("div", {
3664
+ className: "grid gap-1 sm:grid-cols-2",
3665
+ children: connections.map((connection) => /* @__PURE__ */ jsxs("label", {
3666
+ className: "flex items-center gap-2 text-sm text-text-primary",
3667
+ children: [
3668
+ /* @__PURE__ */ jsx(Checkbox, {
3669
+ checked: form.connectionScope.includes(connection.id),
3670
+ onCheckedChange: (on) => toggleConnection(connection.id, on === true)
3671
+ }),
3672
+ /* @__PURE__ */ jsx("span", { children: connection.name }),
3673
+ /* @__PURE__ */ jsx("span", {
3674
+ className: "text-xs text-text-tertiary",
3675
+ children: connection.provider?.name ?? connection.providerId
3676
+ })
3677
+ ]
3678
+ }, connection.id))
3673
3679
  })]
3674
3680
  }),
3675
- showForm && /* @__PURE__ */ jsxs(Card, {
3676
- className: "border-border-secondary bg-background-secondary",
3677
- children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs("div", {
3678
- className: "flex items-center justify-between gap-3",
3679
- children: [/* @__PURE__ */ jsx("h3", {
3680
- className: "font-medium text-text-primary",
3681
- children: editingId ? tr("fluidgrids.settings.logStreaming.edit", "Edit destination") : tr("fluidgrids.settings.logStreaming.new", "New destination")
3682
- }), /* @__PURE__ */ jsx(Button$1, {
3683
- variant: "ghost",
3684
- size: "sm",
3685
- onClick: resetForm,
3686
- children: /* @__PURE__ */ jsx(Icon, {
3687
- icon: "mdi:close",
3688
- width: 18
3689
- })
3690
- })]
3691
- }) }), /* @__PURE__ */ jsxs(CardContent, {
3692
- className: "space-y-5",
3693
- children: [
3694
- /* @__PURE__ */ jsxs("div", {
3695
- className: "grid gap-4 md:grid-cols-2",
3696
- children: [/* @__PURE__ */ jsxs("div", {
3697
- className: "space-y-2",
3698
- children: [/* @__PURE__ */ jsx(Label, {
3699
- htmlFor: "log-stream-name",
3700
- children: tr("fluidgrids.settings.logStreaming.name", "Name")
3701
- }), /* @__PURE__ */ jsx(Input, {
3702
- id: "log-stream-name",
3703
- value: form.name,
3704
- onChange: (event) => setForm((current) => ({
3705
- ...current,
3706
- name: event.target.value
3707
- })),
3708
- placeholder: "Production error stream"
3709
- })]
3710
- }), /* @__PURE__ */ jsxs("div", {
3711
- className: "space-y-2",
3712
- children: [/* @__PURE__ */ jsx(Label, {
3713
- htmlFor: "log-stream-provider",
3714
- children: tr("fluidgrids.settings.logStreaming.provider", "Provider")
3715
- }), /* @__PURE__ */ jsx("select", {
3716
- id: "log-stream-provider",
3717
- value: form.provider,
3718
- onChange: (event) => setForm((current) => ({
3719
- ...current,
3720
- provider: event.target.value,
3721
- endpoint: ""
3722
- })),
3723
- className: "w-full rounded-md border border-border-secondary bg-background-primary px-3 py-2 text-sm text-text-primary",
3724
- children: PROVIDERS.map((provider) => /* @__PURE__ */ jsx("option", {
3725
- value: provider,
3726
- children: provider
3727
- }, provider))
3728
- })]
3729
- })]
3730
- }),
3731
- /* @__PURE__ */ jsxs("div", {
3732
- className: "space-y-2",
3733
- children: [/* @__PURE__ */ jsx(Label, {
3734
- htmlFor: "log-stream-endpoint",
3735
- children: tr("fluidgrids.settings.logStreaming.endpoint", "Endpoint")
3736
- }), /* @__PURE__ */ jsx(Input, {
3737
- id: "log-stream-endpoint",
3738
- value: form.endpoint,
3739
- onChange: (event) => setForm((current) => ({
3740
- ...current,
3741
- endpoint: event.target.value
3742
- })),
3743
- placeholder: endpointPlaceholder(form.provider),
3744
- type: form.provider === "WEBHOOK" || form.provider === "SENTRY" ? "url" : "text"
3745
- })]
3746
- }),
3747
- form.provider === "WEBHOOK" && /* @__PURE__ */ jsxs("div", {
3748
- className: "space-y-2",
3749
- children: [/* @__PURE__ */ jsx(Label, {
3750
- htmlFor: "log-stream-secret",
3751
- children: tr("fluidgrids.settings.logStreaming.secret", "Signing secret")
3752
- }), /* @__PURE__ */ jsx(Input, {
3753
- id: "log-stream-secret",
3754
- value: form.secret,
3755
- onChange: (event) => setForm((current) => ({
3756
- ...current,
3757
- secret: event.target.value
3758
- })),
3759
- placeholder: editingId ? "Leave blank to keep the current secret" : "Enter a signing secret",
3760
- type: "password",
3761
- autoComplete: "new-password"
3762
- })]
3763
- }),
3764
- /* @__PURE__ */ jsxs("div", {
3765
- className: "space-y-2",
3766
- children: [/* @__PURE__ */ jsx(Label, {
3767
- htmlFor: "log-stream-config",
3768
- children: tr("fluidgrids.settings.logStreaming.config", "Provider configuration (JSON)")
3769
- }), /* @__PURE__ */ jsx("textarea", {
3770
- id: "log-stream-config",
3771
- value: form.configText,
3772
- onChange: (event) => setForm((current) => ({
3773
- ...current,
3774
- configText: event.target.value
3775
- })),
3776
- className: "min-h-24 w-full rounded-md border border-border-secondary bg-background-primary px-3 py-2 font-mono text-xs text-text-primary",
3777
- spellCheck: false
3778
- })]
3779
- }),
3780
- /* @__PURE__ */ jsxs("fieldset", {
3781
- className: "space-y-2",
3782
- children: [/* @__PURE__ */ jsx("legend", {
3783
- className: "text-sm font-medium text-text-primary",
3784
- children: tr("fluidgrids.settings.logStreaming.events", "Events to stream")
3785
- }), /* @__PURE__ */ jsx("div", {
3786
- className: "grid gap-2 sm:grid-cols-2 lg:grid-cols-4",
3787
- children: LOG_STREAM_EVENT_TYPES.map((event) => /* @__PURE__ */ jsxs("label", {
3788
- className: "flex items-center gap-2 text-sm text-text-secondary",
3789
- children: [/* @__PURE__ */ jsx("input", {
3790
- type: "checkbox",
3791
- checked: form.eventFilters.includes(event),
3792
- onChange: () => toggleEvent(event),
3793
- className: "size-4 rounded border-border-secondary accent-action-primary-bg"
3794
- }), eventLabel(event)]
3795
- }, event))
3796
- })]
3797
- }),
3798
- /* @__PURE__ */ jsxs("label", {
3799
- className: "flex items-center gap-2 text-sm text-text-secondary",
3800
- children: [/* @__PURE__ */ jsx("input", {
3801
- type: "checkbox",
3802
- checked: form.isEnabled,
3803
- onChange: (event) => setForm((current) => ({
3804
- ...current,
3805
- isEnabled: event.target.checked
3806
- })),
3807
- className: "size-4 rounded border-border-secondary accent-action-primary-bg"
3808
- }), tr("fluidgrids.settings.logStreaming.enabled", "Destination enabled")]
3809
- }),
3810
- /* @__PURE__ */ jsxs("div", {
3811
- className: "flex justify-end gap-2",
3812
- children: [/* @__PURE__ */ jsx(Button$1, {
3813
- variant: "outline",
3814
- onClick: resetForm,
3815
- children: tr("fluidgrids.settings.logStreaming.cancel", "Cancel")
3816
- }), /* @__PURE__ */ jsx(Button$1, {
3817
- "data-agent": "confirm",
3818
- onClick: () => void save(),
3819
- disabled: actions.loading,
3820
- children: actions.loading ? tr("fluidgrids.settings.logStreaming.saving", "Saving…") : tr("fluidgrids.settings.logStreaming.save", "Save destination")
3821
- })]
3822
- })
3823
- ]
3681
+ /* @__PURE__ */ jsxs("div", {
3682
+ className: "flex items-center gap-2",
3683
+ children: [/* @__PURE__ */ jsx(Switch, {
3684
+ id: "agent-approval",
3685
+ checked: form.requireToolApproval,
3686
+ onCheckedChange: (on) => setForm({
3687
+ ...form,
3688
+ requireToolApproval: on
3689
+ })
3690
+ }), /* @__PURE__ */ jsx(Label, {
3691
+ htmlFor: "agent-approval",
3692
+ children: tr("fluidgrids.agents.requireApproval", "Every tool call waits for a person's approval")
3824
3693
  })]
3825
3694
  }),
3826
3695
  /* @__PURE__ */ jsxs("div", {
3827
- className: "space-y-3",
3696
+ className: "flex gap-2",
3697
+ children: [/* @__PURE__ */ jsx(Button$1, {
3698
+ onClick: () => void submit(),
3699
+ disabled: busy,
3700
+ "data-testid": "agent-definition-save",
3701
+ children: tr("fluidgrids.agents.save", "Save agent")
3702
+ }), /* @__PURE__ */ jsx(Button$1, {
3703
+ variant: "outline",
3704
+ onClick: onCancel,
3705
+ disabled: busy,
3706
+ children: tr("fluidgrids.agents.cancel", "Cancel")
3707
+ })]
3708
+ })
3709
+ ]
3710
+ });
3711
+ }
3712
+ function AgentGrants({ agent, loadGrants, onGrant, onRevoke }) {
3713
+ const tr = useTr();
3714
+ const [grants, setGrants] = useState(null);
3715
+ const [days, setDays] = useState(String(30));
3716
+ const [busy, setBusy] = useState(false);
3717
+ const load = useCallback(async () => {
3718
+ try {
3719
+ setGrants(await loadGrants(agent.id));
3720
+ } catch (err) {
3721
+ setGrants([]);
3722
+ toast.error(errorMessage$1(err));
3723
+ }
3724
+ }, [agent.id, loadGrants]);
3725
+ useEffect(() => {
3726
+ load();
3727
+ }, [load]);
3728
+ const run = async (action, done) => {
3729
+ setBusy(true);
3730
+ try {
3731
+ await action();
3732
+ toast.success(done);
3733
+ await load();
3734
+ } catch (err) {
3735
+ toast.error(errorMessage$1(err));
3736
+ } finally {
3737
+ setBusy(false);
3738
+ }
3739
+ };
3740
+ const mine = grants?.find((grant) => grant.isMine && grant.status === "ACTIVE");
3741
+ return /* @__PURE__ */ jsxs("div", {
3742
+ className: "space-y-2",
3743
+ "data-testid": `agent-grants-${agent.id}`,
3744
+ children: [/* @__PURE__ */ jsx("div", {
3745
+ className: "flex flex-wrap items-center gap-2",
3746
+ children: mine ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
3747
+ className: "text-sm text-text-primary",
3748
+ children: mine.expiresAt ? tr("fluidgrids.agents.myGrantUntil", "This agent may act for you until {{date}}.", { date: new Date(mine.expiresAt).toLocaleDateString() }) : tr("fluidgrids.agents.myGrant", "This agent may act for you.")
3749
+ }), /* @__PURE__ */ jsx(Button$1, {
3750
+ variant: "outline",
3751
+ size: "sm",
3752
+ disabled: busy,
3753
+ onClick: () => void run(() => onRevoke(agent.id, null), tr("fluidgrids.agents.withdrawn", "Grant withdrawn")),
3754
+ "data-testid": "agent-grant-withdraw",
3755
+ children: tr("fluidgrids.agents.withdraw", "Withdraw my grant")
3756
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3757
+ /* @__PURE__ */ jsx(Label, {
3758
+ htmlFor: `grant-days-${agent.id}`,
3759
+ className: "text-sm",
3760
+ children: tr("fluidgrids.agents.grantFor", "Let it act for me for")
3761
+ }),
3762
+ /* @__PURE__ */ jsx(Input, {
3763
+ id: `grant-days-${agent.id}`,
3764
+ className: "w-20",
3765
+ type: "number",
3766
+ min: 1,
3767
+ max: 365,
3768
+ value: days,
3769
+ onChange: (e) => setDays(e.target.value)
3770
+ }),
3771
+ /* @__PURE__ */ jsx("span", {
3772
+ className: "text-sm text-text-secondary",
3773
+ children: tr("fluidgrids.agents.days", "days")
3774
+ }),
3775
+ /* @__PURE__ */ jsx(Button$1, {
3776
+ size: "sm",
3777
+ disabled: busy || agent.status !== "ACTIVE",
3778
+ onClick: () => void run(async () => onGrant(agent.id, grantDays(days)), tr("fluidgrids.agents.granted", "Grant given")),
3779
+ "data-testid": "agent-grant-give",
3780
+ children: tr("fluidgrids.agents.grant", "Grant")
3781
+ })
3782
+ ] })
3783
+ }), grants && grants.length > 0 ? /* @__PURE__ */ jsx("ul", {
3784
+ className: "space-y-1 text-xs text-text-secondary",
3785
+ children: grants.map((grant) => /* @__PURE__ */ jsxs("li", {
3786
+ className: "flex items-center gap-2",
3828
3787
  children: [
3829
- loading && destinations.length === 0 && /* @__PURE__ */ jsx("div", {
3830
- className: "rounded-lg border border-border-secondary bg-background-secondary p-6 text-sm text-text-secondary",
3831
- children: tr("fluidgrids.settings.logStreaming.loading", "Loading destinations…")
3788
+ /* @__PURE__ */ jsx("span", {
3789
+ className: "font-mono",
3790
+ children: grant.userId
3832
3791
  }),
3833
- !loading && destinations.length === 0 && /* @__PURE__ */ jsxs("div", {
3834
- className: "rounded-lg border border-dashed border-border-secondary bg-background-secondary p-8 text-center",
3835
- children: [
3836
- /* @__PURE__ */ jsx(Icon, {
3837
- icon: "mdi:transit-connection-variant",
3838
- className: "mx-auto mb-3 text-text-secondary",
3839
- width: 32
3840
- }),
3841
- /* @__PURE__ */ jsx("p", {
3842
- className: "font-medium text-text-primary",
3843
- children: tr("fluidgrids.settings.logStreaming.emptyTitle", "No log destinations yet")
3844
- }),
3845
- /* @__PURE__ */ jsx("p", {
3846
- className: "mt-1 text-sm text-text-secondary",
3847
- children: tr("fluidgrids.settings.logStreaming.emptyDescription", "Add a webhook, syslog target, or Sentry DSN to start streaming events.")
3848
- })
3849
- ]
3792
+ /* @__PURE__ */ jsx(Badge, {
3793
+ variant: "outline",
3794
+ children: grant.status
3850
3795
  }),
3851
- destinations.map((destination) => /* @__PURE__ */ jsx(Card, {
3852
- className: "border-border-secondary bg-background-secondary",
3853
- children: /* @__PURE__ */ jsxs(CardContent, {
3854
- className: "flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between",
3796
+ grant.expiresAt ? /* @__PURE__ */ jsx("span", { children: tr("fluidgrids.agents.until", "until {{date}}", { date: new Date(grant.expiresAt).toLocaleDateString() }) }) : null,
3797
+ !grant.isMine && grant.status === "ACTIVE" ? /* @__PURE__ */ jsx(Button$1, {
3798
+ variant: "ghost",
3799
+ size: "sm",
3800
+ disabled: busy,
3801
+ onClick: () => void run(() => onRevoke(agent.id, grant.userId), tr("fluidgrids.agents.revoked", "Grant revoked")),
3802
+ children: tr("fluidgrids.agents.revoke", "Revoke")
3803
+ }) : null
3804
+ ]
3805
+ }, grant.userId))
3806
+ }) : null]
3807
+ });
3808
+ }
3809
+ function AgentsTab() {
3810
+ const tr = useTr();
3811
+ const { agents, loading, error, save, remove, grants, grant, revoke } = useAgentDefinitions();
3812
+ const [editing, setEditing] = useState(null);
3813
+ const onSave = async (form) => {
3814
+ const input = formToInput$2(form);
3815
+ await save(editing?.id ?? null, input);
3816
+ toast.success(tr("fluidgrids.agents.saved", "Agent saved"));
3817
+ setEditing(null);
3818
+ };
3819
+ const onDelete = async (agent) => {
3820
+ try {
3821
+ await remove(agent.id);
3822
+ toast.success(tr("fluidgrids.agents.deleted", "Agent deleted"));
3823
+ } catch (err) {
3824
+ toast.error(errorMessage$1(err));
3825
+ }
3826
+ };
3827
+ return /* @__PURE__ */ jsxs("div", {
3828
+ className: "space-y-4",
3829
+ "data-testid": "agents-tab",
3830
+ children: [
3831
+ /* @__PURE__ */ jsxs("div", {
3832
+ className: "flex flex-wrap items-start justify-between gap-2",
3833
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
3834
+ className: "text-base font-semibold text-text-primary",
3835
+ children: tr("fluidgrids.agents.title", "Agents")
3836
+ }), /* @__PURE__ */ jsx("p", {
3837
+ className: "text-sm text-text-secondary",
3838
+ children: tr("fluidgrids.agents.subtitle", "Give an AI Agent node an identity of its own. It can only call the tools and connections you allow, only for people who granted it, and every tool call is recorded under the agent and the person.")
3839
+ })] }), !editing ? /* @__PURE__ */ jsxs(Button$1, {
3840
+ onClick: () => setEditing({
3841
+ id: null,
3842
+ form: emptyAgentForm()
3843
+ }),
3844
+ "data-testid": "agent-definition-new",
3845
+ children: [/* @__PURE__ */ jsx(Icon, {
3846
+ icon: "mdi:plus",
3847
+ width: 16
3848
+ }), tr("fluidgrids.agents.new", "New agent")]
3849
+ }) : null]
3850
+ }),
3851
+ editing ? /* @__PURE__ */ jsx(AgentEditor, {
3852
+ initial: editing.form,
3853
+ onCancel: () => setEditing(null),
3854
+ onSave
3855
+ }) : null,
3856
+ error ? /* @__PURE__ */ jsx("p", {
3857
+ className: "text-sm text-destructive",
3858
+ children: errorMessage$1(error)
3859
+ }) : null,
3860
+ loading ? /* @__PURE__ */ jsx("p", {
3861
+ className: "text-sm text-text-secondary",
3862
+ children: tr("fluidgrids.agents.loading", "Loading agents…")
3863
+ }) : agents.length === 0 && !error ? /* @__PURE__ */ jsx("p", {
3864
+ className: "text-sm text-text-secondary",
3865
+ children: tr("fluidgrids.agents.empty", "No agents yet.")
3866
+ }) : /* @__PURE__ */ jsx("ul", {
3867
+ className: "space-y-3",
3868
+ children: agents.map((agent) => /* @__PURE__ */ jsxs("li", {
3869
+ className: "rounded-lg border border-border-secondary bg-background-secondary p-3 space-y-2",
3870
+ "data-testid": `agent-definition-${agent.id}`,
3871
+ children: [
3872
+ /* @__PURE__ */ jsxs("div", {
3873
+ className: "flex flex-wrap items-center justify-between gap-2",
3855
3874
  children: [/* @__PURE__ */ jsxs("div", {
3856
- className: "min-w-0",
3875
+ className: "flex items-center gap-2",
3857
3876
  children: [
3858
- /* @__PURE__ */ jsxs("div", {
3859
- className: "flex flex-wrap items-center gap-2",
3860
- children: [
3861
- /* @__PURE__ */ jsx("h3", {
3862
- className: "font-medium text-text-primary",
3863
- children: destination.name
3864
- }),
3865
- /* @__PURE__ */ jsx("span", {
3866
- className: "rounded-full bg-background-tertiary px-2 py-0.5 text-xs text-text-secondary",
3867
- children: destination.provider
3868
- }),
3869
- /* @__PURE__ */ jsx("span", {
3870
- className: destination.isEnabled ? "text-xs text-status-success-text" : "text-xs text-text-secondary",
3871
- children: destination.isEnabled ? tr("fluidgrids.settings.logStreaming.enabledBadge", "Enabled") : tr("fluidgrids.settings.logStreaming.disabledBadge", "Disabled")
3872
- })
3873
- ]
3874
- }),
3875
- /* @__PURE__ */ jsx("p", {
3876
- className: "mt-1 truncate text-sm text-text-secondary",
3877
- children: destination.endpoint
3877
+ /* @__PURE__ */ jsx(Icon, {
3878
+ icon: "mdi:robot-outline",
3879
+ width: 18
3878
3880
  }),
3879
- /* @__PURE__ */ jsxs("p", {
3880
- className: "mt-1 text-xs text-text-secondary",
3881
- children: [destination.eventFilters.length > 0 ? destination.eventFilters.map(eventLabel).join(", ") : tr("fluidgrids.settings.logStreaming.allEvents", "All events"), destination.hasSecret && " · Secret configured"]
3881
+ /* @__PURE__ */ jsx("span", {
3882
+ className: "font-medium text-text-primary",
3883
+ children: agent.name
3882
3884
  }),
3883
- destination.lastDispatchError && /* @__PURE__ */ jsx("p", {
3884
- className: "mt-2 text-xs text-status-error-text",
3885
- children: destination.lastDispatchError
3886
- })
3887
- ]
3888
- }), /* @__PURE__ */ jsxs("div", {
3889
- className: "flex shrink-0 flex-wrap gap-2",
3890
- children: [
3891
- /* @__PURE__ */ jsxs(Button$1, {
3885
+ /* @__PURE__ */ jsx(Badge, {
3892
3886
  variant: "outline",
3893
- size: "sm",
3894
- onClick: () => void sendTest(destination),
3895
- disabled: actions.loading,
3896
- children: [/* @__PURE__ */ jsx(Icon, {
3897
- icon: "mdi:send-check-outline",
3898
- className: "mr-1",
3899
- width: 15
3900
- }), tr("fluidgrids.settings.logStreaming.test", "Send test")]
3887
+ children: agent.status
3901
3888
  }),
3902
- /* @__PURE__ */ jsx(Button$1, {
3889
+ agent.requireToolApproval ? /* @__PURE__ */ jsx(Badge, {
3903
3890
  variant: "outline",
3904
- size: "sm",
3905
- onClick: () => {
3906
- setEditingId(destination.id);
3907
- setShowForm(true);
3908
- },
3909
- disabled: actions.loading,
3910
- children: tr("fluidgrids.settings.logStreaming.editButton", "Edit")
3911
- }),
3912
- /* @__PURE__ */ jsxs(Button$1, {
3913
- variant: "ghost",
3914
- size: "sm",
3915
- onClick: () => void remove(destination),
3916
- disabled: actions.loading,
3917
- children: [/* @__PURE__ */ jsx(Icon, {
3918
- icon: "mdi:trash-can-outline",
3919
- className: "text-status-error-text",
3920
- width: 17
3921
- }), /* @__PURE__ */ jsx("span", {
3922
- className: "sr-only",
3923
- children: tr("fluidgrids.settings.logStreaming.delete", "Delete")
3924
- })]
3925
- })
3891
+ children: tr("fluidgrids.agents.approvalBadge", "Approval required")
3892
+ }) : null
3926
3893
  ]
3894
+ }), /* @__PURE__ */ jsxs("div", {
3895
+ className: "flex gap-1",
3896
+ children: [/* @__PURE__ */ jsx(Button$1, {
3897
+ variant: "ghost",
3898
+ size: "sm",
3899
+ onClick: () => setEditing({
3900
+ id: agent.id,
3901
+ form: agentToForm(agent)
3902
+ }),
3903
+ children: tr("fluidgrids.agents.edit", "Edit")
3904
+ }), /* @__PURE__ */ jsx(Button$1, {
3905
+ variant: "ghost",
3906
+ size: "sm",
3907
+ onClick: () => void onDelete(agent),
3908
+ children: tr("fluidgrids.agents.delete", "Delete")
3909
+ })]
3927
3910
  })]
3911
+ }),
3912
+ agent.description ? /* @__PURE__ */ jsx("p", {
3913
+ className: "text-sm text-text-secondary",
3914
+ children: agent.description
3915
+ }) : null,
3916
+ /* @__PURE__ */ jsx("div", {
3917
+ className: "text-xs text-text-secondary",
3918
+ children: agent.toolAllowlist.length > 0 ? agent.toolAllowlist.map(describeRule).join(" · ") : tr("fluidgrids.agents.noTools", "No tools allowed")
3919
+ }),
3920
+ /* @__PURE__ */ jsx("div", {
3921
+ className: "text-xs text-text-tertiary",
3922
+ children: tr("fluidgrids.agents.connectionCount", "{{count}} connections in scope", { count: agent.connectionScope.length })
3923
+ }),
3924
+ /* @__PURE__ */ jsx(AgentGrants, {
3925
+ agent,
3926
+ loadGrants: grants,
3927
+ onGrant: grant,
3928
+ onRevoke: revoke
3928
3929
  })
3929
- }, destination.id))
3930
- ]
3930
+ ]
3931
+ }, agent.id))
3931
3932
  })
3932
3933
  ]
3933
3934
  });