@acmekit/dashboard 2.13.33 → 2.13.35

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 (87) hide show
  1. package/dist/{api-key-management-create-4AG76FJV.mjs → api-key-management-create-U37VC624.mjs} +3 -3
  2. package/dist/{api-key-management-detail-T2TB4KST.mjs → api-key-management-detail-ZYKL4ATI.mjs} +10 -10
  3. package/dist/{api-key-management-edit-R44OHS7B.mjs → api-key-management-edit-TSZGMIBL.mjs} +3 -3
  4. package/dist/{api-key-management-list-QK4Q7Y5I.mjs → api-key-management-list-HCJFJWWB.mjs} +3 -3
  5. package/dist/app.css +31 -0
  6. package/dist/app.js +3726 -1386
  7. package/dist/app.mjs +240 -38
  8. package/dist/{chunk-GBFVWROS.mjs → chunk-5IEHCYJO.mjs} +1 -1
  9. package/dist/{chunk-DQCEH3X2.mjs → chunk-7F3CWXUH.mjs} +1 -1
  10. package/dist/chunk-A7ULKHDE.mjs +126 -0
  11. package/dist/{chunk-DN3MIYQH.mjs → chunk-FKTMBR44.mjs} +1 -1
  12. package/dist/chunk-GBPAZAJK.mjs +34 -0
  13. package/dist/{chunk-YRWSG3YM.mjs → chunk-HHPPTD3B.mjs} +1 -1
  14. package/dist/chunk-LP6CPB7N.mjs +213 -0
  15. package/dist/{chunk-EFRMWHRX.mjs → chunk-PFZQYK7R.mjs} +1 -1
  16. package/dist/{chunk-XIM7X4FB.mjs → chunk-SYACY6AL.mjs} +1 -1
  17. package/dist/{chunk-2U3RK3JG.mjs → chunk-VEI6HW6L.mjs} +3 -5
  18. package/dist/{chunk-ST2YB7JN.mjs → chunk-WLRJXEKL.mjs} +1 -1
  19. package/dist/{chunk-ULSPL3DR.mjs → chunk-XIP35KXF.mjs} +1 -1
  20. package/dist/{chunk-DTY37DDZ.mjs → chunk-YKIWIMJX.mjs} +1 -0
  21. package/dist/en.json +132 -3
  22. package/dist/{invite-XGPZZBUP.mjs → invite-3JSNOA2B.mjs} +3 -3
  23. package/dist/{login-GNP3QIPI.mjs → login-BEJ5EFGE.mjs} +9 -9
  24. package/dist/{profile-detail-YX27F7N6.mjs → profile-detail-QVTJC4JC.mjs} +3 -3
  25. package/dist/{profile-edit-2VRDU75O.mjs → profile-edit-MIO62TWH.mjs} +3 -3
  26. package/dist/{reset-password-TWRNZO6Z.mjs → reset-password-BN4KAJQL.mjs} +2 -2
  27. package/dist/{settings-3XWLL5LG.mjs → settings-GH5IWXHE.mjs} +3 -3
  28. package/dist/{translation-list-CCEQJNED.mjs → translation-list-JA22BUKN.mjs} +10 -10
  29. package/dist/{translations-edit-E57GVUFV.mjs → translations-edit-STTMANVT.mjs} +11 -11
  30. package/dist/{user-detail-KUSRRVNX.mjs → user-detail-WCXBFRGS.mjs} +3 -3
  31. package/dist/{user-edit-HTN3ZGCL.mjs → user-edit-XDVMJOS4.mjs} +3 -3
  32. package/dist/{user-invite-E3FAAU3V.mjs → user-invite-73ZDSDFC.mjs} +3 -3
  33. package/dist/{user-list-KNJ5S3IM.mjs → user-list-MPJXE3CA.mjs} +5 -5
  34. package/dist/{user-metadata-5GQK75DT.mjs → user-metadata-ADNTL3LT.mjs} +10 -10
  35. package/dist/workflow-analytics-4WCI4ODQ.mjs +152 -0
  36. package/dist/workflow-definition-detail-GI6CFBMG.mjs +94 -0
  37. package/dist/workflow-definition-list-GF3XAEPS.mjs +142 -0
  38. package/dist/workflow-execution-complete-step-WSRLO572.mjs +245 -0
  39. package/dist/workflow-execution-detail-3RH6EQSS.mjs +1411 -0
  40. package/dist/workflow-execution-list-AQEGAME4.mjs +596 -0
  41. package/dist/workflow-execution-rerun-WCYLYL3Q.mjs +138 -0
  42. package/dist/workflow-execution-run-MWN5KWNY.mjs +135 -0
  43. package/dist/workflow-scheduled-list-ZPXR7CZM.mjs +174 -0
  44. package/package.json +9 -9
  45. package/src/components/layout/main-layout/main-layout.tsx +28 -1
  46. package/src/dashboard-app/routes/get-route.map.tsx +71 -0
  47. package/src/hooks/api/workflow-definitions.tsx +79 -0
  48. package/src/hooks/api/workflow-executions.tsx +145 -1
  49. package/src/hooks/api/workflow-metrics.tsx +48 -0
  50. package/src/hooks/use-workflow-sse.tsx +78 -0
  51. package/src/i18n/translations/$schema.json +534 -4
  52. package/src/i18n/translations/en.json +132 -3
  53. package/src/routes/workflow-analytics/workflow-analytics.tsx +167 -0
  54. package/src/routes/workflow-definitions/workflow-definition-detail/workflow-definition-detail.tsx +98 -0
  55. package/src/routes/workflow-definitions/workflow-definition-list/components/workflow-definition-list-table/use-workflow-definition-table-columns.tsx +78 -0
  56. package/src/routes/workflow-definitions/workflow-definition-list/components/workflow-definition-list-table/workflow-definition-list-table.tsx +65 -0
  57. package/src/routes/workflow-definitions/workflow-definition-list/workflow-definition-list.tsx +15 -0
  58. package/src/routes/workflow-executions/constants.ts +16 -0
  59. package/src/routes/workflow-executions/utils.ts +170 -14
  60. package/src/routes/workflow-executions/workflow-execution-complete-step/workflow-execution-complete-step.tsx +270 -0
  61. package/src/routes/workflow-executions/workflow-execution-detail/breadcrumb.tsx +7 -1
  62. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-action-bar/index.ts +1 -0
  63. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-action-bar/workflow-execution-action-bar.tsx +212 -0
  64. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-error-card/index.ts +1 -0
  65. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-error-card/workflow-execution-error-card.tsx +59 -0
  66. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-history-section/workflow-execution-history-section.tsx +157 -6
  67. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-payload-section/workflow-execution-payload-section.tsx +122 -6
  68. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-timeline-section/workflow-execution-timeline-section.tsx +7 -1
  69. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-waiting-banner/index.ts +1 -0
  70. package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-waiting-banner/workflow-execution-waiting-banner.tsx +63 -0
  71. package/src/routes/workflow-executions/workflow-execution-detail/workflow-detail.tsx +46 -1
  72. package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/use-workflow-execution-table-columns.tsx +7 -0
  73. package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/use-workflow-execution-table-filters.tsx +7 -1
  74. package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/use-workflow-execution-table-query.tsx +4 -2
  75. package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/workflow-execution-auto-refresh.tsx +73 -0
  76. package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/workflow-execution-list-table.tsx +17 -1
  77. package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/workflow-execution-row-actions.tsx +116 -0
  78. package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/workflow-execution-saved-views.tsx +84 -0
  79. package/src/routes/workflow-executions/workflow-execution-list/workflow-execution-list.tsx +1 -1
  80. package/src/routes/workflow-executions/workflow-execution-rerun/workflow-execution-rerun.tsx +159 -0
  81. package/src/routes/workflow-executions/workflow-execution-run/workflow-execution-run.tsx +139 -0
  82. package/src/routes/workflow-scheduled/workflow-scheduled-list.tsx +269 -0
  83. package/dist/chunk-LKWTBYYC.mjs +0 -35
  84. package/dist/chunk-RPAL6FHW.mjs +0 -73
  85. package/dist/workflow-execution-detail-5O5VCXL3.mjs +0 -870
  86. package/dist/workflow-execution-list-DETG4MRT.mjs +0 -347
  87. /package/dist/{chunk-22YYMH6M.mjs → chunk-RISX76YT.mjs} +0 -0
@@ -0,0 +1,59 @@
1
+ import { XCircle } from "@acmekit/icons"
2
+ import { HttpTypes } from "@acmekit/types"
3
+ import { Container, Heading, Text } from "@acmekit/ui"
4
+ import { useTranslation } from "react-i18next"
5
+ import { TransactionState } from "../../../types"
6
+ import { getFailedSteps } from "../../../utils"
7
+
8
+ type WorkflowExecutionErrorCardProps = {
9
+ execution: HttpTypes.AdminWorkflowExecution
10
+ }
11
+
12
+ export const WorkflowExecutionErrorCard = ({
13
+ execution,
14
+ }: WorkflowExecutionErrorCardProps) => {
15
+ const { t } = useTranslation()
16
+
17
+ if ((execution.state as TransactionState) !== TransactionState.FAILED) {
18
+ return null
19
+ }
20
+
21
+ const failedSteps = getFailedSteps(execution)
22
+ if (failedSteps.length === 0) return null
23
+
24
+ const firstFailure = failedSteps[0]
25
+ const errorMessage =
26
+ firstFailure.error?.error?.message ||
27
+ firstFailure.error?.error ||
28
+ "Unknown error"
29
+ const handlerType = firstFailure.error?.handlerType || "invoke"
30
+
31
+ return (
32
+ <Container className="border border-ui-tag-red-border bg-ui-tag-red-bg p-0">
33
+ <div className="flex items-start gap-x-3 px-6 py-4">
34
+ <div className="mt-0.5">
35
+ <XCircle className="text-ui-tag-red-icon" />
36
+ </div>
37
+ <div className="flex flex-1 flex-col gap-y-1">
38
+ <Heading level="h3">
39
+ {t("workflowExecutions.error.failureAtStep", {
40
+ step: firstFailure.stepId,
41
+ })}
42
+ </Heading>
43
+ <Text size="small" className="text-ui-fg-subtle">
44
+ {String(errorMessage)}
45
+ </Text>
46
+ <Text size="xsmall" className="text-ui-fg-muted">
47
+ {handlerType === "compensate" ? "Compensation" : "Invoke"} failure
48
+ </Text>
49
+ </div>
50
+ <a
51
+ href={`#${firstFailure.stepId}`}
52
+ className="text-ui-fg-interactive txt-compact-small-plus shrink-0 hover:underline"
53
+ >
54
+ {t("workflowExecutions.error.jumpToStep")}
55
+ </a>
56
+ </div>
57
+ </Container>
58
+ )
59
+ }
@@ -1,12 +1,15 @@
1
1
  import { Spinner, TriangleDownMini } from "@acmekit/icons"
2
2
  import { HttpTypes } from "@acmekit/types"
3
3
  import {
4
+ Button,
4
5
  clx,
5
6
  CodeBlock,
6
7
  Container,
8
+ Copy,
7
9
  Heading,
8
10
  IconButton,
9
11
  Text,
12
+ toast,
10
13
  } from "@acmekit/ui"
11
14
  import { format } from "date-fns"
12
15
  import { Collapsible as RadixCollapsible } from "radix-ui"
@@ -20,7 +23,12 @@ import {
20
23
  STEP_OK_STATES,
21
24
  STEP_SKIPPED_STATES,
22
25
  } from "../../../constants"
23
- import { TransactionStepState, TransactionStepStatus } from "../../../types"
26
+ import { TransactionState, TransactionStepState, TransactionStepStatus } from "../../../types"
27
+ import {
28
+ computeIdempotencyKey,
29
+ formatStepDuration,
30
+ } from "../../../utils"
31
+ import { useRetryStep } from "../../../../../hooks/api/workflow-executions"
24
32
 
25
33
  type WorkflowExecutionHistorySectionProps = {
26
34
  execution: HttpTypes.AdminWorkflowExecution
@@ -34,6 +42,14 @@ export const WorkflowExecutionHistorySection = ({
34
42
  const map = Object.values(execution.execution?.steps || {})
35
43
  const steps = map.filter((step) => step.id !== "_root")
36
44
 
45
+ const hasCompensation = [
46
+ TransactionState.COMPENSATING,
47
+ TransactionState.REVERTED,
48
+ TransactionState.WAITING_TO_COMPENSATE,
49
+ ].includes(execution.state as TransactionState)
50
+
51
+ const [showCompensation, setShowCompensation] = useState(false)
52
+
37
53
  // check if any of the steps have a .invoke.state of "permanent_failure" and if that is the case then return its id
38
54
  const unreachableStepId = steps.find(
39
55
  (step) => step.invoke.status === TransactionStepStatus.PERMANENT_FAILURE
@@ -55,6 +71,17 @@ export const WorkflowExecutionHistorySection = ({
55
71
  <Heading level="h2">
56
72
  {t("workflowExecutions.history.sectionTitle")}
57
73
  </Heading>
74
+ {hasCompensation && (
75
+ <Button
76
+ size="small"
77
+ variant="secondary"
78
+ onClick={() => setShowCompensation((v) => !v)}
79
+ >
80
+ {showCompensation
81
+ ? t("workflowExecutions.history.showInvoke")
82
+ : t("workflowExecutions.history.showCompensation")}
83
+ </Button>
84
+ )}
58
85
  </div>
59
86
  <div className="flex flex-col gap-y-0.5 px-6 py-4">
60
87
  {steps.map((step, index) => {
@@ -77,6 +104,8 @@ export const WorkflowExecutionHistorySection = ({
77
104
  stepError={error}
78
105
  isLast={index === steps.length - 1}
79
106
  isUnreachable={unreachableSteps.includes(step.id)}
107
+ execution={execution}
108
+ showCompensation={showCompensation}
80
109
  />
81
110
  )
82
111
  })}
@@ -91,12 +120,16 @@ const Event = ({
91
120
  stepError,
92
121
  isLast,
93
122
  isUnreachable,
123
+ execution,
124
+ showCompensation,
94
125
  }: {
95
126
  step: HttpTypes.AdminWorkflowExecutionStep
96
127
  stepInvokeContext: HttpTypes.StepInvokeResult | undefined
97
128
  stepError?: HttpTypes.StepError | undefined
98
129
  isLast: boolean
99
130
  isUnreachable?: boolean
131
+ execution: HttpTypes.AdminWorkflowExecution
132
+ showCompensation?: boolean
100
133
  }) => {
101
134
  const [open, setOpen] = useState(false)
102
135
 
@@ -114,6 +147,37 @@ const Event = ({
114
147
  }, [hash, stepId])
115
148
 
116
149
  const identifier = step.id.split(".").pop()
150
+ const isPermanentFailure =
151
+ step.invoke.status === TransactionStepStatus.PERMANENT_FAILURE
152
+ const isWaiting = step.invoke.status === TransactionStepStatus.WAITING
153
+
154
+ const idempotencyKey = computeIdempotencyKey(
155
+ execution.workflow_id,
156
+ execution.transaction_id,
157
+ stepId,
158
+ "invoke"
159
+ )
160
+
161
+ const duration = formatStepDuration(
162
+ step.startedAt ?? undefined,
163
+ (step as any).endedAt ?? undefined
164
+ )
165
+
166
+ const { mutateAsync: retryStep, isPending: isRetrying } = useRetryStep(
167
+ execution.workflow_id,
168
+ execution.transaction_id
169
+ )
170
+
171
+ const handleRetryStep = async () => {
172
+ await retryStep(
173
+ { step_id: stepId },
174
+ {
175
+ onSuccess: () => {
176
+ toast.success(t("workflowExecutions.actions.retryStepSuccess"))
177
+ },
178
+ }
179
+ )
180
+ }
117
181
 
118
182
  return (
119
183
  <div
@@ -131,9 +195,10 @@ const Event = ({
131
195
  "bg-ui-tag-green-icon": STEP_OK_STATES.includes(
132
196
  step.invoke.state
133
197
  ),
134
- "bg-ui-tag-orange-icon": STEP_IN_PROGRESS_STATES.includes(
135
- step.invoke.state
136
- ),
198
+ "bg-ui-tag-purple-icon": isWaiting,
199
+ "bg-ui-tag-orange-icon":
200
+ !isWaiting &&
201
+ STEP_IN_PROGRESS_STATES.includes(step.invoke.state),
137
202
  "bg-ui-tag-red-icon": STEP_ERROR_STATES.includes(
138
203
  step.invoke.state
139
204
  ),
@@ -154,15 +219,29 @@ const Event = ({
154
219
  />
155
220
  </div>
156
221
  </div>
157
- <RadixCollapsible.Root open={open} onOpenChange={setOpen}>
222
+ <RadixCollapsible.Root
223
+ open={open}
224
+ onOpenChange={(isOpen) => {
225
+ setOpen(isOpen)
226
+ if (isOpen) {
227
+ window.history.replaceState(null, "", `#${stepId}`)
228
+ }
229
+ }}
230
+ >
158
231
  <RadixCollapsible.Trigger asChild>
159
232
  <div className="group flex cursor-pointer items-start justify-between outline-none">
160
233
  <Text size="small" leading="compact" weight="plus">
161
234
  {identifier}
162
235
  </Text>
163
236
  <div className="flex items-center gap-x-2">
237
+ {duration && (
238
+ <Text size="small" leading="compact" className="text-ui-fg-muted">
239
+ {duration}
240
+ </Text>
241
+ )}
164
242
  <StepState
165
243
  state={step.invoke.state}
244
+ status={step.invoke.status}
166
245
  startedAt={step.startedAt}
167
246
  isUnreachable={isUnreachable}
168
247
  />
@@ -174,6 +253,35 @@ const Event = ({
174
253
  </RadixCollapsible.Trigger>
175
254
  <RadixCollapsible.Content ref={ref}>
176
255
  <div className="flex flex-col gap-y-2 pb-4 pt-2">
256
+ {/* Idempotency key */}
257
+ <div className="flex items-center justify-between">
258
+ <Text size="xsmall" className="text-ui-fg-muted">
259
+ {t("workflowExecutions.history.idempotencyKeyLabel")}
260
+ </Text>
261
+ <Copy content={idempotencyKey} asChild>
262
+ <Text
263
+ size="xsmall"
264
+ className="text-ui-fg-subtle cursor-pointer font-mono"
265
+ >
266
+ {idempotencyKey}
267
+ </Text>
268
+ </Copy>
269
+ </div>
270
+
271
+ {/* Retry from this step — only for permanent failures */}
272
+ {isPermanentFailure && (
273
+ <div className="flex justify-end">
274
+ <Button
275
+ size="small"
276
+ variant="secondary"
277
+ isLoading={isRetrying}
278
+ onClick={handleRetryStep}
279
+ >
280
+ {t("workflowExecutions.history.retryFromStep")}
281
+ </Button>
282
+ </div>
283
+ )}
284
+
177
285
  <div className="text-ui-fg-subtle flex flex-col gap-y-2">
178
286
  <Text size="small" leading="compact">
179
287
  {t("workflowExecutions.history.definitionLabel")}
@@ -215,7 +323,37 @@ const Event = ({
215
323
  </CodeBlock>
216
324
  </div>
217
325
  )}
218
- {!!stepInvokeContext?.output?.compensateInput &&
326
+ {showCompensation && step.compensate.state !== TransactionStepState.NOT_STARTED && (
327
+ <div className="border-ui-tag-orange-border bg-ui-tag-orange-bg rounded-md border p-3">
328
+ <Text size="xsmall" weight="plus" className="text-ui-tag-orange-text mb-2">
329
+ ↩ {t("workflowExecutions.history.revertedLabel")} — {step.compensate.state}
330
+ </Text>
331
+ {!!stepInvokeContext?.output?.compensateInput && (
332
+ <div className="flex flex-col gap-y-1">
333
+ <Text size="xsmall" className="text-ui-fg-subtle">
334
+ {t("workflowExecutions.history.compensateInputLabel")}
335
+ </Text>
336
+ <CodeBlock
337
+ snippets={[
338
+ {
339
+ code: JSON.stringify(
340
+ stepInvokeContext?.output?.compensateInput ?? {},
341
+ null,
342
+ 2
343
+ ),
344
+ label: t("workflowExecutions.history.compensateInputLabel"),
345
+ language: "json",
346
+ hideLineNumbers: true,
347
+ },
348
+ ]}
349
+ >
350
+ <CodeBlock.Body />
351
+ </CodeBlock>
352
+ </div>
353
+ )}
354
+ </div>
355
+ )}
356
+ {!showCompensation && !!stepInvokeContext?.output?.compensateInput &&
219
357
  step.compensate.state === TransactionStepState.REVERTED && (
220
358
  <div className="text-ui-fg-subtle flex flex-col gap-y-2">
221
359
  <Text size="small" leading="compact">
@@ -277,10 +415,12 @@ const Event = ({
277
415
 
278
416
  const StepState = ({
279
417
  state,
418
+ status,
280
419
  startedAt,
281
420
  isUnreachable,
282
421
  }: {
283
422
  state: HttpTypes.TransactionStepState
423
+ status?: HttpTypes.TransactionStepStatus
284
424
  startedAt?: number | null
285
425
  isUnreachable?: boolean
286
426
  }) => {
@@ -288,6 +428,7 @@ const StepState = ({
288
428
 
289
429
  const isFailed = state === TransactionStepState.FAILED
290
430
  const isRunning = state === TransactionStepState.INVOKING
431
+ const isWaitingResponse = status === TransactionStepStatus.WAITING
291
432
  const isSkipped = state === TransactionStepState.SKIPPED
292
433
  const isSkippedFailure = state === TransactionStepState.SKIPPED_FAILURE
293
434
 
@@ -295,6 +436,16 @@ const StepState = ({
295
436
  return null
296
437
  }
297
438
 
439
+ if (isWaitingResponse) {
440
+ return (
441
+ <div className="flex items-center gap-x-1">
442
+ <Text size="small" leading="compact" className="text-ui-tag-purple-text">
443
+ {t("workflowExecutions.history.awaitingState")}
444
+ </Text>
445
+ </div>
446
+ )
447
+ }
448
+
298
449
  if (isRunning) {
299
450
  return (
300
451
  <div className="flex items-center gap-x-1">
@@ -1,24 +1,140 @@
1
1
  import { HttpTypes } from "@acmekit/types"
2
+ import { Button, Container, Heading, Tabs, toast } from "@acmekit/ui"
3
+ import { useState } from "react"
4
+ import { useTranslation } from "react-i18next"
2
5
  import { JsonViewSection } from "../../../../../components/common/json-view-section"
3
6
 
4
7
  type WorkflowExecutionPayloadSectionProps = {
5
8
  execution: HttpTypes.AdminWorkflowExecution
6
9
  }
7
10
 
11
+ function downloadJson(data: unknown, filename: string) {
12
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
13
+ type: "application/json",
14
+ })
15
+ const url = URL.createObjectURL(blob)
16
+ const a = document.createElement("a")
17
+ a.href = url
18
+ a.download = filename
19
+ a.click()
20
+ URL.revokeObjectURL(url)
21
+ }
22
+
8
23
  export const WorkflowExecutionPayloadSection = ({
9
24
  execution,
10
25
  }: WorkflowExecutionPayloadSectionProps) => {
26
+ const { t } = useTranslation()
27
+
11
28
  let payload = execution.context?.data?.payload
29
+ if (payload && typeof payload !== "object") {
30
+ payload = { input: payload }
31
+ }
32
+
33
+ const checkpoint = execution.context || {}
34
+ const errors = (execution as any).context?.errors || []
35
+
36
+ const stepOutputs: Record<string, unknown> = {}
37
+ const steps = execution?.execution?.steps || {}
38
+ for (const [stepId, step] of Object.entries(steps)) {
39
+ if (stepId === "_root") continue
40
+ const output = (step as any)?.invoke?.output
41
+ if (output !== undefined) {
42
+ stepOutputs[stepId] = output
43
+ }
44
+ }
45
+
46
+ const hasPayload = !!payload
47
+ const hasStepOutputs = Object.keys(stepOutputs).length > 0
48
+ const hasErrors = errors.length > 0
12
49
 
13
- if (!payload) {
50
+ const defaultTab = hasPayload ? "input" : "checkpoint"
51
+ const [activeTab, setActiveTab] = useState(defaultTab)
52
+
53
+ if (!hasPayload && !hasStepOutputs && !hasErrors) {
14
54
  return null
15
55
  }
16
56
 
17
- // payloads may be a primitive, so we need to wrap them in an object
18
- // to ensure the JsonViewSection component can render them.
19
- if (typeof payload !== "object") {
20
- payload = { input: payload }
57
+ const activeData =
58
+ activeTab === "input"
59
+ ? payload
60
+ : activeTab === "checkpoint"
61
+ ? checkpoint
62
+ : activeTab === "outputs"
63
+ ? stepOutputs
64
+ : errors
65
+
66
+ const handleCopyAll = () => {
67
+ navigator.clipboard.writeText(JSON.stringify(activeData, null, 2))
68
+ toast.success("Copied to clipboard")
69
+ }
70
+
71
+ const handleDownload = () => {
72
+ downloadJson(
73
+ activeData,
74
+ `${execution.workflow_id}-${activeTab}.json`
75
+ )
21
76
  }
22
77
 
23
- return <JsonViewSection data={payload as object} />
78
+ return (
79
+ <Container className="divide-y p-0">
80
+ <div className="flex items-center justify-between px-6 py-4">
81
+ <Heading level="h2">
82
+ {t("workflowExecutions.payload.inputPayload")}
83
+ </Heading>
84
+ <div className="flex items-center gap-x-2">
85
+ <Button size="small" variant="transparent" onClick={handleCopyAll}>
86
+ {t("workflowExecutions.payload.copyAll")}
87
+ </Button>
88
+ <Button size="small" variant="transparent" onClick={handleDownload}>
89
+ {t("workflowExecutions.payload.downloadJson")}
90
+ </Button>
91
+ </div>
92
+ </div>
93
+ <Tabs
94
+ value={activeTab}
95
+ onValueChange={setActiveTab}
96
+ >
97
+ <div className="border-ui-border-base border-b px-6">
98
+ <Tabs.List>
99
+ {hasPayload && (
100
+ <Tabs.Trigger value="input">
101
+ {t("workflowExecutions.payload.inputPayload")}
102
+ </Tabs.Trigger>
103
+ )}
104
+ <Tabs.Trigger value="checkpoint">
105
+ {t("workflowExecutions.payload.fullCheckpoint")}
106
+ </Tabs.Trigger>
107
+ {hasStepOutputs && (
108
+ <Tabs.Trigger value="outputs">
109
+ {t("workflowExecutions.payload.stepOutputs")}
110
+ </Tabs.Trigger>
111
+ )}
112
+ {hasErrors && (
113
+ <Tabs.Trigger value="errors">
114
+ {t("workflowExecutions.payload.errors")}
115
+ </Tabs.Trigger>
116
+ )}
117
+ </Tabs.List>
118
+ </div>
119
+ {hasPayload && (
120
+ <Tabs.Content value="input" className="p-0">
121
+ <JsonViewSection data={payload as object} />
122
+ </Tabs.Content>
123
+ )}
124
+ <Tabs.Content value="checkpoint" className="p-0">
125
+ <JsonViewSection data={checkpoint as object} />
126
+ </Tabs.Content>
127
+ {hasStepOutputs && (
128
+ <Tabs.Content value="outputs" className="p-0">
129
+ <JsonViewSection data={stepOutputs} />
130
+ </Tabs.Content>
131
+ )}
132
+ {hasErrors && (
133
+ <Tabs.Content value="errors" className="p-0">
134
+ <JsonViewSection data={errors as object} />
135
+ </Tabs.Content>
136
+ )}
137
+ </Tabs>
138
+ </Container>
139
+ )
24
140
  }
@@ -18,6 +18,7 @@ import {
18
18
  STEP_OK_STATES,
19
19
  STEP_SKIPPED_STATES,
20
20
  } from "../../../constants"
21
+ import { TransactionStepState } from "../../../types"
21
22
  import { useDocumentDirection } from "../../../../../hooks/use-document-direction"
22
23
 
23
24
  type WorkflowExecutionTimelineSectionProps = {
@@ -398,6 +399,8 @@ const Node = ({ step }: { step: HttpTypes.AdminWorkflowExecutionStep }) => {
398
399
  }, 100)
399
400
  }
400
401
 
402
+ const isInvoking = step.invoke.state === TransactionStepState.INVOKING
403
+
401
404
  return (
402
405
  <Link
403
406
  to={`#${stepId}`}
@@ -405,7 +408,10 @@ const Node = ({ step }: { step: HttpTypes.AdminWorkflowExecutionStep }) => {
405
408
  className="focus-visible:shadow-borders-focus transition-fg rounded-md outline-none"
406
409
  >
407
410
  <div
408
- className="bg-ui-bg-base shadow-borders-base flex min-w-[120px] items-center gap-x-0.5 rounded-md p-0.5"
411
+ className={clx(
412
+ "bg-ui-bg-base shadow-borders-base flex min-w-[120px] items-center gap-x-0.5 rounded-md p-0.5",
413
+ { "animate-pulse": isInvoking }
414
+ )}
409
415
  data-step-id={step.id}
410
416
  >
411
417
  <div className="flex size-5 items-center justify-center">
@@ -0,0 +1 @@
1
+ export { WorkflowExecutionWaitingBanner } from "./workflow-execution-waiting-banner"
@@ -0,0 +1,63 @@
1
+ import { ClockSolidMini } from "@acmekit/icons"
2
+ import { HttpTypes } from "@acmekit/types"
3
+ import { Container, Copy, Heading, Text } from "@acmekit/ui"
4
+ import { useTranslation } from "react-i18next"
5
+ import { Link } from "react-router-dom"
6
+ import { computeIdempotencyKey, getWaitingSteps } from "../../../utils"
7
+
8
+ type WorkflowExecutionWaitingBannerProps = {
9
+ execution: HttpTypes.AdminWorkflowExecution
10
+ }
11
+
12
+ export const WorkflowExecutionWaitingBanner = ({
13
+ execution,
14
+ }: WorkflowExecutionWaitingBannerProps) => {
15
+ const { t } = useTranslation()
16
+ const waitingSteps = getWaitingSteps(execution)
17
+
18
+ if (waitingSteps.length === 0) return null
19
+
20
+ const firstStep = waitingSteps[0]
21
+ const idempotencyKey = computeIdempotencyKey(
22
+ execution.workflow_id,
23
+ execution.transaction_id,
24
+ firstStep.stepId,
25
+ "invoke"
26
+ )
27
+
28
+ return (
29
+ <Container className="border border-ui-tag-purple-border bg-ui-tag-purple-bg p-0">
30
+ <div className="flex items-start gap-x-3 px-6 py-4">
31
+ <div className="mt-0.5">
32
+ <ClockSolidMini className="text-ui-tag-purple-icon" />
33
+ </div>
34
+ <div className="flex flex-1 flex-col gap-y-1">
35
+ <Heading level="h3">
36
+ {t("workflowExecutions.asyncStep.waitingBanner")}
37
+ </Heading>
38
+ <Text size="small" className="text-ui-fg-subtle">
39
+ {t("workflowExecutions.asyncStep.waitingDescription", {
40
+ step: firstStep.stepId,
41
+ })}
42
+ </Text>
43
+ <div className="mt-1 flex items-center gap-x-2">
44
+ <Text size="xsmall" className="text-ui-fg-muted">
45
+ {t("workflowExecutions.asyncStep.idempotencyKey")}:
46
+ </Text>
47
+ <Copy content={idempotencyKey} asChild>
48
+ <code className="bg-ui-bg-base txt-compact-xsmall cursor-pointer rounded px-1.5 py-0.5">
49
+ {idempotencyKey}
50
+ </code>
51
+ </Copy>
52
+ </div>
53
+ </div>
54
+ <Link
55
+ to="complete-step"
56
+ className="text-ui-fg-interactive txt-compact-small-plus shrink-0 hover:underline"
57
+ >
58
+ {t("workflowExecutions.actions.completeStep")}
59
+ </Link>
60
+ </div>
61
+ </Container>
62
+ )
63
+ }
@@ -1,9 +1,20 @@
1
1
  import { useParams } from "react-router-dom"
2
+ import { useQueryClient } from "@tanstack/react-query"
2
3
 
3
4
  import { SingleColumnPageSkeleton } from "../../../components/common/skeleton"
4
5
  import { SingleColumnPage } from "../../../components/layout/pages"
5
- import { useWorkflowExecution } from "../../../hooks/api/workflow-executions"
6
+ import {
7
+ useWorkflowExecution,
8
+ workflowExecutionsQueryKeys,
9
+ } from "../../../hooks/api/workflow-executions"
6
10
  import { useExtension } from "../../../providers/extension-provider"
11
+ import { useWorkflowSSE } from "../../../hooks/use-workflow-sse"
12
+ import { TRANSACTION_ACTIVE_STATES } from "../constants"
13
+ import { TransactionState } from "../types"
14
+ import { isTerminalState, mergeStepEvent } from "../utils"
15
+ import { WorkflowExecutionActionBar } from "./components/workflow-execution-action-bar"
16
+ import { WorkflowExecutionErrorCard } from "./components/workflow-execution-error-card"
17
+ import { WorkflowExecutionWaitingBanner } from "./components/workflow-execution-waiting-banner"
7
18
  import { WorkflowExecutionGeneralSection } from "./components/workflow-execution-general-section"
8
19
  import { WorkflowExecutionHistorySection } from "./components/workflow-execution-history-section"
9
20
  import { WorkflowExecutionPayloadSection } from "./components/workflow-execution-payload-section"
@@ -11,12 +22,42 @@ import { WorkflowExecutionTimelineSection } from "./components/workflow-executio
11
22
 
12
23
  export const ExecutionDetail = () => {
13
24
  const { id } = useParams()
25
+ const queryClient = useQueryClient()
14
26
 
15
27
  const { workflow_execution, isLoading, isError, error } =
16
28
  useWorkflowExecution(id!)
17
29
 
18
30
  const { getWidgets } = useExtension()
19
31
 
32
+ const isActive = workflow_execution
33
+ ? TRANSACTION_ACTIVE_STATES.includes(
34
+ workflow_execution.state as TransactionState
35
+ )
36
+ : false
37
+
38
+ useWorkflowSSE(workflow_execution?.workflow_id || "", {
39
+ enabled: isActive && !!workflow_execution?.workflow_id,
40
+ onEvent: (event) => {
41
+ queryClient.setQueryData(
42
+ workflowExecutionsQueryKeys.detail(id!),
43
+ (current: any) => {
44
+ if (!current) return current
45
+ return mergeStepEvent(current, event)
46
+ }
47
+ )
48
+
49
+ if (
50
+ event.event_type === "onFinish" ||
51
+ (event.response?.state &&
52
+ isTerminalState(event.response.state as TransactionState))
53
+ ) {
54
+ queryClient.invalidateQueries({
55
+ queryKey: workflowExecutionsQueryKeys.detail(id!),
56
+ })
57
+ }
58
+ },
59
+ })
60
+
20
61
  if (isLoading || !workflow_execution) {
21
62
  return <SingleColumnPageSkeleton sections={4} showJSON />
22
63
  }
@@ -33,7 +74,11 @@ export const ExecutionDetail = () => {
33
74
  }}
34
75
  data={workflow_execution}
35
76
  showJSON
77
+ hasOutlet
36
78
  >
79
+ <WorkflowExecutionActionBar execution={workflow_execution} />
80
+ <WorkflowExecutionErrorCard execution={workflow_execution} />
81
+ <WorkflowExecutionWaitingBanner execution={workflow_execution} />
37
82
  <WorkflowExecutionGeneralSection execution={workflow_execution} />
38
83
  <WorkflowExecutionTimelineSection execution={workflow_execution} />
39
84
  <WorkflowExecutionPayloadSection execution={workflow_execution} />
@@ -7,6 +7,7 @@ import { TransactionStepState } from "../../../types"
7
7
  import { getTransactionState, getTransactionStateColor } from "../../../utils"
8
8
  import { HttpTypes } from "@acmekit/types"
9
9
  import { DataTableStatusCell } from "../../../../../components/data-table/components/data-table-status-cell/data-table-status-cell"
10
+ import { WorkflowExecutionRowActions } from "./workflow-execution-row-actions"
10
11
 
11
12
  const columnHelper =
12
13
  createColumnHelper<
@@ -186,6 +187,12 @@ export const useWorkflowExecutionTableColumns = (): ColumnDef<
186
187
  return <DateCell date={date} />
187
188
  },
188
189
  }),
190
+ columnHelper.display({
191
+ id: "actions",
192
+ cell: ({ row }) => (
193
+ <WorkflowExecutionRowActions execution={row.original} />
194
+ ),
195
+ }),
189
196
  ],
190
197
  [t]
191
198
  )