@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.
- package/dist/{api-key-management-create-4AG76FJV.mjs → api-key-management-create-U37VC624.mjs} +3 -3
- package/dist/{api-key-management-detail-T2TB4KST.mjs → api-key-management-detail-ZYKL4ATI.mjs} +10 -10
- package/dist/{api-key-management-edit-R44OHS7B.mjs → api-key-management-edit-TSZGMIBL.mjs} +3 -3
- package/dist/{api-key-management-list-QK4Q7Y5I.mjs → api-key-management-list-HCJFJWWB.mjs} +3 -3
- package/dist/app.css +31 -0
- package/dist/app.js +3726 -1386
- package/dist/app.mjs +240 -38
- package/dist/{chunk-GBFVWROS.mjs → chunk-5IEHCYJO.mjs} +1 -1
- package/dist/{chunk-DQCEH3X2.mjs → chunk-7F3CWXUH.mjs} +1 -1
- package/dist/chunk-A7ULKHDE.mjs +126 -0
- package/dist/{chunk-DN3MIYQH.mjs → chunk-FKTMBR44.mjs} +1 -1
- package/dist/chunk-GBPAZAJK.mjs +34 -0
- package/dist/{chunk-YRWSG3YM.mjs → chunk-HHPPTD3B.mjs} +1 -1
- package/dist/chunk-LP6CPB7N.mjs +213 -0
- package/dist/{chunk-EFRMWHRX.mjs → chunk-PFZQYK7R.mjs} +1 -1
- package/dist/{chunk-XIM7X4FB.mjs → chunk-SYACY6AL.mjs} +1 -1
- package/dist/{chunk-2U3RK3JG.mjs → chunk-VEI6HW6L.mjs} +3 -5
- package/dist/{chunk-ST2YB7JN.mjs → chunk-WLRJXEKL.mjs} +1 -1
- package/dist/{chunk-ULSPL3DR.mjs → chunk-XIP35KXF.mjs} +1 -1
- package/dist/{chunk-DTY37DDZ.mjs → chunk-YKIWIMJX.mjs} +1 -0
- package/dist/en.json +132 -3
- package/dist/{invite-XGPZZBUP.mjs → invite-3JSNOA2B.mjs} +3 -3
- package/dist/{login-GNP3QIPI.mjs → login-BEJ5EFGE.mjs} +9 -9
- package/dist/{profile-detail-YX27F7N6.mjs → profile-detail-QVTJC4JC.mjs} +3 -3
- package/dist/{profile-edit-2VRDU75O.mjs → profile-edit-MIO62TWH.mjs} +3 -3
- package/dist/{reset-password-TWRNZO6Z.mjs → reset-password-BN4KAJQL.mjs} +2 -2
- package/dist/{settings-3XWLL5LG.mjs → settings-GH5IWXHE.mjs} +3 -3
- package/dist/{translation-list-CCEQJNED.mjs → translation-list-JA22BUKN.mjs} +10 -10
- package/dist/{translations-edit-E57GVUFV.mjs → translations-edit-STTMANVT.mjs} +11 -11
- package/dist/{user-detail-KUSRRVNX.mjs → user-detail-WCXBFRGS.mjs} +3 -3
- package/dist/{user-edit-HTN3ZGCL.mjs → user-edit-XDVMJOS4.mjs} +3 -3
- package/dist/{user-invite-E3FAAU3V.mjs → user-invite-73ZDSDFC.mjs} +3 -3
- package/dist/{user-list-KNJ5S3IM.mjs → user-list-MPJXE3CA.mjs} +5 -5
- package/dist/{user-metadata-5GQK75DT.mjs → user-metadata-ADNTL3LT.mjs} +10 -10
- package/dist/workflow-analytics-4WCI4ODQ.mjs +152 -0
- package/dist/workflow-definition-detail-GI6CFBMG.mjs +94 -0
- package/dist/workflow-definition-list-GF3XAEPS.mjs +142 -0
- package/dist/workflow-execution-complete-step-WSRLO572.mjs +245 -0
- package/dist/workflow-execution-detail-3RH6EQSS.mjs +1411 -0
- package/dist/workflow-execution-list-AQEGAME4.mjs +596 -0
- package/dist/workflow-execution-rerun-WCYLYL3Q.mjs +138 -0
- package/dist/workflow-execution-run-MWN5KWNY.mjs +135 -0
- package/dist/workflow-scheduled-list-ZPXR7CZM.mjs +174 -0
- package/package.json +9 -9
- package/src/components/layout/main-layout/main-layout.tsx +28 -1
- package/src/dashboard-app/routes/get-route.map.tsx +71 -0
- package/src/hooks/api/workflow-definitions.tsx +79 -0
- package/src/hooks/api/workflow-executions.tsx +145 -1
- package/src/hooks/api/workflow-metrics.tsx +48 -0
- package/src/hooks/use-workflow-sse.tsx +78 -0
- package/src/i18n/translations/$schema.json +534 -4
- package/src/i18n/translations/en.json +132 -3
- package/src/routes/workflow-analytics/workflow-analytics.tsx +167 -0
- package/src/routes/workflow-definitions/workflow-definition-detail/workflow-definition-detail.tsx +98 -0
- package/src/routes/workflow-definitions/workflow-definition-list/components/workflow-definition-list-table/use-workflow-definition-table-columns.tsx +78 -0
- package/src/routes/workflow-definitions/workflow-definition-list/components/workflow-definition-list-table/workflow-definition-list-table.tsx +65 -0
- package/src/routes/workflow-definitions/workflow-definition-list/workflow-definition-list.tsx +15 -0
- package/src/routes/workflow-executions/constants.ts +16 -0
- package/src/routes/workflow-executions/utils.ts +170 -14
- package/src/routes/workflow-executions/workflow-execution-complete-step/workflow-execution-complete-step.tsx +270 -0
- package/src/routes/workflow-executions/workflow-execution-detail/breadcrumb.tsx +7 -1
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-action-bar/index.ts +1 -0
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-action-bar/workflow-execution-action-bar.tsx +212 -0
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-error-card/index.ts +1 -0
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-error-card/workflow-execution-error-card.tsx +59 -0
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-history-section/workflow-execution-history-section.tsx +157 -6
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-payload-section/workflow-execution-payload-section.tsx +122 -6
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-timeline-section/workflow-execution-timeline-section.tsx +7 -1
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-waiting-banner/index.ts +1 -0
- package/src/routes/workflow-executions/workflow-execution-detail/components/workflow-execution-waiting-banner/workflow-execution-waiting-banner.tsx +63 -0
- package/src/routes/workflow-executions/workflow-execution-detail/workflow-detail.tsx +46 -1
- package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/use-workflow-execution-table-columns.tsx +7 -0
- package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/use-workflow-execution-table-filters.tsx +7 -1
- package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/use-workflow-execution-table-query.tsx +4 -2
- package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/workflow-execution-auto-refresh.tsx +73 -0
- package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/workflow-execution-list-table.tsx +17 -1
- package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/workflow-execution-row-actions.tsx +116 -0
- package/src/routes/workflow-executions/workflow-execution-list/components/workflow-execution-list-table/workflow-execution-saved-views.tsx +84 -0
- package/src/routes/workflow-executions/workflow-execution-list/workflow-execution-list.tsx +1 -1
- package/src/routes/workflow-executions/workflow-execution-rerun/workflow-execution-rerun.tsx +159 -0
- package/src/routes/workflow-executions/workflow-execution-run/workflow-execution-run.tsx +139 -0
- package/src/routes/workflow-scheduled/workflow-scheduled-list.tsx +269 -0
- package/dist/chunk-LKWTBYYC.mjs +0 -35
- package/dist/chunk-RPAL6FHW.mjs +0 -73
- package/dist/workflow-execution-detail-5O5VCXL3.mjs +0 -870
- package/dist/workflow-execution-list-DETG4MRT.mjs +0 -347
- /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-
|
|
135
|
-
|
|
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
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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=
|
|
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 {
|
|
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
|
)
|