@acmekit/dashboard 2.13.21 → 2.13.23

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.
@@ -2965,6 +2965,13 @@
2965
2965
  "transactionIdLabel": "Transaction ID",
2966
2966
  "workflowIdLabel": "Workflow ID",
2967
2967
  "progressLabel": "Progress",
2968
+ "metrics": {
2969
+ "stepsSummary": "{{completed}} of {{total}} steps",
2970
+ "failedSteps": "{{count}} failed",
2971
+ "skippedSteps": "{{count}} skipped",
2972
+ "compensatedYes": "Compensated",
2973
+ "compensatedNo": "Not compensated"
2974
+ },
2968
2975
  "stepsCompletedLabel_one": "{{completed}} of {{count}} step",
2969
2976
  "stepsCompletedLabel_other": "{{completed}} of {{count}} steps",
2970
2977
  "list": {
@@ -3180,6 +3187,7 @@
3180
3187
  "note": "Note",
3181
3188
  "automaticTaxes": "Automatic Taxes",
3182
3189
  "taxInclusivePricing": "Tax inclusive pricing",
3190
+ "duration": "Duration",
3183
3191
  "currency": "Currency",
3184
3192
  "address": "Address",
3185
3193
  "address2": "Apartment, suite, etc.",
@@ -1,16 +1,10 @@
1
- import {
2
- Badge,
3
- Container,
4
- Copy,
5
- Heading,
6
- StatusBadge,
7
- Text,
8
- clx,
9
- } from "@acmekit/ui"
1
+ import { Badge, Container, Copy, Heading, StatusBadge, Text, clx } from "@acmekit/ui"
10
2
  import { useTranslation } from "react-i18next"
11
3
  import { getTransactionState, getTransactionStateColor } from "../../../utils"
12
4
  import { HttpTypes } from "@acmekit/types"
13
5
  import { TransactionState, TransactionStepState } from "../../../types"
6
+ import { useDate } from "../../../../../hooks/use-date"
7
+ import { STEP_ERROR_STATES, STEP_SKIPPED_STATES } from "../../../constants"
14
8
 
15
9
  type WorkflowExecutionGeneralSectionProps = {
16
10
  execution: HttpTypes.AdminWorkflowExecution
@@ -20,6 +14,7 @@ export const WorkflowExecutionGeneralSection = ({
20
14
  execution,
21
15
  }: WorkflowExecutionGeneralSectionProps) => {
22
16
  const { t } = useTranslation()
17
+ const { getFullDate } = useDate()
23
18
 
24
19
  const cleanId = execution.id.replace("wf_exec_", "")
25
20
  const translatedState = getTransactionState(
@@ -30,6 +25,27 @@ export const WorkflowExecutionGeneralSection = ({
30
25
  execution.state as TransactionState
31
26
  )
32
27
 
28
+ const createdAt = execution.created_at
29
+ const updatedAt = execution.updated_at
30
+
31
+ const allSteps = Object.values(execution.execution?.steps || {}).filter(
32
+ (step) => step.id !== ROOT_PREFIX
33
+ )
34
+
35
+ const totalSteps = allSteps.length
36
+ const completedSteps = allSteps.filter(
37
+ (step) => step.invoke.state === TransactionStepState.DONE
38
+ ).length
39
+ const failedSteps = allSteps.filter((step) =>
40
+ STEP_ERROR_STATES.includes(step.invoke.state as HttpTypes.TransactionStepState)
41
+ ).length
42
+ const skippedSteps = allSteps.filter((step) =>
43
+ STEP_SKIPPED_STATES.includes(step.invoke.state as HttpTypes.TransactionStepState)
44
+ ).length
45
+ const compensatedSteps = allSteps.filter(
46
+ (step) => step.compensate.state === TransactionStepState.REVERTED
47
+ ).length
48
+
33
49
  return (
34
50
  <Container className="divide-y p-0">
35
51
  <div className="flex items-center justify-between px-6 py-4">
@@ -43,17 +59,26 @@ export const WorkflowExecutionGeneralSection = ({
43
59
  <Text size="small" leading="compact" weight="plus">
44
60
  {t("workflowExecutions.workflowIdLabel")}
45
61
  </Text>
46
- <Badge size="2xsmall" className="w-fit">
47
- {execution.workflow_id}
48
- </Badge>
62
+ <div className="flex items-center gap-x-1">
63
+ <Badge size="2xsmall" className="w-fit">
64
+ {execution.workflow_id}
65
+ </Badge>
66
+ <Copy content={execution.workflow_id} className="text-ui-fg-muted" />
67
+ </div>
49
68
  </div>
50
69
  <div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
51
70
  <Text size="small" leading="compact" weight="plus">
52
71
  {t("workflowExecutions.transactionIdLabel")}
53
72
  </Text>
54
- <Badge size="2xsmall" className="w-fit">
55
- {execution.transaction_id}
56
- </Badge>
73
+ <div className="flex items-center gap-x-1">
74
+ <Badge size="2xsmall" className="w-fit">
75
+ {execution.transaction_id}
76
+ </Badge>
77
+ <Copy
78
+ content={execution.transaction_id}
79
+ className="text-ui-fg-muted"
80
+ />
81
+ </div>
57
82
  </div>
58
83
  <div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
59
84
  <Text size="small" leading="compact" weight="plus">
@@ -61,6 +86,58 @@ export const WorkflowExecutionGeneralSection = ({
61
86
  </Text>
62
87
  <Progress steps={execution.execution?.steps} />
63
88
  </div>
89
+ <div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
90
+ <Text size="small" leading="compact" weight="plus">
91
+ {t("fields.summary")}
92
+ </Text>
93
+ <div className="flex flex-col gap-y-1">
94
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
95
+ {t("workflowExecutions.metrics.stepsSummary", {
96
+ completed: completedSteps,
97
+ total: totalSteps,
98
+ })}
99
+ </Text>
100
+ {failedSteps > 0 && (
101
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
102
+ {t("workflowExecutions.metrics.failedSteps", {
103
+ count: failedSteps,
104
+ })}
105
+ </Text>
106
+ )}
107
+ {skippedSteps > 0 && (
108
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
109
+ {t("workflowExecutions.metrics.skippedSteps", {
110
+ count: skippedSteps,
111
+ })}
112
+ </Text>
113
+ )}
114
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
115
+ {compensatedSteps > 0
116
+ ? t("workflowExecutions.metrics.compensatedYes")
117
+ : t("workflowExecutions.metrics.compensatedNo")}
118
+ </Text>
119
+ </div>
120
+ </div>
121
+ <div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
122
+ <Text size="small" leading="compact" weight="plus">
123
+ {t("fields.createdAt")}
124
+ </Text>
125
+ <Text size="small" leading="compact">
126
+ {createdAt
127
+ ? getFullDate({ date: createdAt, includeTime: true })
128
+ : "-"}
129
+ </Text>
130
+ </div>
131
+ <div className="text-ui-fg-subtle grid grid-cols-2 px-6 py-4">
132
+ <Text size="small" leading="compact" weight="plus">
133
+ {t("fields.updatedAt")}
134
+ </Text>
135
+ <Text size="small" leading="compact">
136
+ {updatedAt
137
+ ? getFullDate({ date: updatedAt, includeTime: true })
138
+ : "-"}
139
+ </Text>
140
+ </div>
64
141
  </Container>
65
142
  )
66
143
  }
@@ -79,7 +156,7 @@ const Progress = ({
79
156
  <Text size="small" leading="compact" className="text-ui-fg-subtle">
80
157
  {t("workflowExecutions.stepsCompletedLabel", {
81
158
  completed: 0,
82
- total: 0,
159
+ count: 0,
83
160
  })}
84
161
  </Text>
85
162
  )
@@ -1,11 +1,12 @@
1
- import { Badge } from "@acmekit/ui"
1
+ import { Badge, Copy, Text, clx } from "@acmekit/ui"
2
2
  import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
3
3
  import { useMemo } from "react"
4
4
  import { useTranslation } from "react-i18next"
5
- import { StatusCell } from "../../../../../components/table/table-cells/common/status-cell"
5
+ import { DateCell } from "../../../../../components/table/table-cells/common/date-cell/date-cell"
6
6
  import { TransactionStepState } from "../../../types"
7
7
  import { getTransactionState, getTransactionStateColor } from "../../../utils"
8
8
  import { HttpTypes } from "@acmekit/types"
9
+ import { DataTableStatusCell } from "../../../../../components/data-table/components/data-table-status-cell/data-table-status-cell"
9
10
 
10
11
  const columnHelper =
11
12
  createColumnHelper<
@@ -20,9 +21,20 @@ export const useWorkflowExecutionTableColumns = (): ColumnDef<
20
21
 
21
22
  return useMemo(
22
23
  () => [
23
- columnHelper.accessor("transaction_id", {
24
- header: t("workflowExecutions.transactionIdLabel"),
25
- cell: ({ getValue }) => <Badge size="2xsmall">{getValue()}</Badge>,
24
+ columnHelper.accessor("workflow_id", {
25
+ header: t("workflowExecutions.workflowIdLabel"),
26
+ cell: ({ getValue }) => {
27
+ const workflowId = getValue()
28
+
29
+ return (
30
+ <div className="flex items-center gap-x-1">
31
+ <Badge size="2xsmall" className="truncate">
32
+ {workflowId}
33
+ </Badge>
34
+ <Copy content={workflowId} className="text-ui-fg-muted" />
35
+ </div>
36
+ )
37
+ },
26
38
  }),
27
39
  columnHelper.accessor("state", {
28
40
  header: t("fields.state"),
@@ -33,9 +45,9 @@ export const useWorkflowExecutionTableColumns = (): ColumnDef<
33
45
  const translatedState = getTransactionState(t, state)
34
46
 
35
47
  return (
36
- <StatusCell color={color}>
48
+ <DataTableStatusCell color={color}>
37
49
  <span className="capitalize">{translatedState}</span>
38
- </StatusCell>
50
+ </DataTableStatusCell>
39
51
  )
40
52
  },
41
53
  }),
@@ -47,7 +59,14 @@ export const useWorkflowExecutionTableColumns = (): ColumnDef<
47
59
  | undefined
48
60
 
49
61
  if (!steps) {
50
- return "0 of 0 steps"
62
+ return (
63
+ <span className="text-ui-fg-subtle whitespace-nowrap">
64
+ {t("workflowExecutions.stepsCompletedLabel", {
65
+ completed: 0,
66
+ count: 0,
67
+ })}
68
+ </span>
69
+ )
51
70
  }
52
71
 
53
72
  const actionableSteps = Object.values(steps).filter(
@@ -58,10 +77,113 @@ export const useWorkflowExecutionTableColumns = (): ColumnDef<
58
77
  (step) => step.invoke.state === TransactionStepState.DONE
59
78
  )
60
79
 
61
- return t("workflowExecutions.stepsCompletedLabel", {
62
- completed: completedSteps.length,
63
- count: actionableSteps.length,
64
- })
80
+ return (
81
+ <div className="flex items-center gap-x-2">
82
+ <div className="flex items-center gap-x-[3px]">
83
+ {actionableSteps.map((step) => (
84
+ <div
85
+ key={step.id}
86
+ className={clx(
87
+ "bg-ui-bg-switch-off shadow-details-switch-background h-3 w-1.5 rounded-full",
88
+ {
89
+ "bg-ui-fg-muted":
90
+ step.invoke.state === TransactionStepState.DONE,
91
+ }
92
+ )}
93
+ data-completed={
94
+ step.invoke.state === TransactionStepState.DONE
95
+ }
96
+ />
97
+ ))}
98
+ </div>
99
+ <span className="text-ui-fg-subtle whitespace-nowrap">
100
+ {t("workflowExecutions.stepsCompletedLabel", {
101
+ completed: completedSteps.length,
102
+ count: actionableSteps.length,
103
+ })}
104
+ </span>
105
+ </div>
106
+ )
107
+ },
108
+ }),
109
+ columnHelper.display({
110
+ id: "duration",
111
+ header: t("fields.duration"),
112
+ cell: ({ row }) => {
113
+ const createdAt = row.original.created_at as Date | string | undefined
114
+ const updatedAt = row.original.updated_at as Date | string | undefined
115
+ const state = row.original.state as HttpTypes.TransactionState
116
+
117
+ if (!createdAt) {
118
+ return (
119
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
120
+ {"-"}
121
+ </Text>
122
+ )
123
+ }
124
+
125
+ const start = new Date(createdAt)
126
+ const end =
127
+ updatedAt &&
128
+ ["done", "failed", "reverted"].includes(state as string)
129
+ ? new Date(updatedAt)
130
+ : new Date()
131
+
132
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) {
133
+ return (
134
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
135
+ {"-"}
136
+ </Text>
137
+ )
138
+ }
139
+
140
+ const ms = end.getTime() - start.getTime()
141
+
142
+ if (ms < 0) {
143
+ return (
144
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
145
+ {"-"}
146
+ </Text>
147
+ )
148
+ }
149
+
150
+ const seconds = Math.floor(ms / 1000)
151
+ const minutes = Math.floor(seconds / 60)
152
+ const hours = Math.floor(minutes / 60)
153
+
154
+ let label: string
155
+
156
+ if (hours > 0) {
157
+ label = `${hours}h ${minutes % 60}m`
158
+ } else if (minutes > 0) {
159
+ label = `${minutes}m ${seconds % 60}s`
160
+ } else if (seconds > 0) {
161
+ label = `${seconds}s`
162
+ } else if (ms > 0) {
163
+ label = "<1s"
164
+ } else {
165
+ label = "0s"
166
+ }
167
+
168
+ return (
169
+ <Text size="small" leading="compact" className="text-ui-fg-subtle">
170
+ {label}
171
+ </Text>
172
+ )
173
+ },
174
+ }),
175
+ columnHelper.accessor("created_at", {
176
+ header: t("fields.createdAt"),
177
+ cell: ({ getValue }) => {
178
+ const date = getValue()
179
+ return <DateCell date={date} />
180
+ },
181
+ }),
182
+ columnHelper.accessor("updated_at", {
183
+ header: t("fields.updatedAt"),
184
+ cell: ({ getValue }) => {
185
+ const date = getValue()
186
+ return <DateCell date={date} />
65
187
  },
66
188
  }),
67
189
  ],
@@ -0,0 +1,57 @@
1
+ import { useTranslation } from "react-i18next"
2
+ import type { Filter } from "../../../../../components/table/data-table"
3
+
4
+ /**
5
+ * Filters for the workflow executions list table.
6
+ *
7
+ * This follows the same pattern as other legacy DataTable filter hooks
8
+ * (for example: useOrderTableFilters, useApiKeyManagementTableFilters).
9
+ */
10
+ export const useWorkflowExecutionTableFilters = (): Filter[] => {
11
+ const { t } = useTranslation()
12
+
13
+ const stateFilter: Filter = {
14
+ key: "state",
15
+ label: t("fields.status"),
16
+ type: "select",
17
+ multiple: true,
18
+ options: [
19
+ {
20
+ label: t("workflowExecutions.state.done"),
21
+ value: "done",
22
+ },
23
+ {
24
+ label: t("workflowExecutions.state.failed"),
25
+ value: "failed",
26
+ },
27
+ {
28
+ label: t("workflowExecutions.state.reverted"),
29
+ value: "reverted",
30
+ },
31
+ {
32
+ label: t("workflowExecutions.state.invoking"),
33
+ value: "invoking",
34
+ },
35
+ {
36
+ label: t("workflowExecutions.transaction.state.waitingToCompensate"),
37
+ value: "waiting_to_compensate",
38
+ },
39
+ {
40
+ label: t("workflowExecutions.state.compensating"),
41
+ value: "compensating",
42
+ },
43
+ {
44
+ label: t("workflowExecutions.state.notStarted"),
45
+ value: "not_started",
46
+ },
47
+ ],
48
+ }
49
+
50
+ const dateFilters: Filter[] = [
51
+ { key: "created_at", label: t("fields.createdAt"), type: "date" },
52
+ { key: "updated_at", label: t("fields.updatedAt"), type: "date" },
53
+ ]
54
+
55
+ return [stateFilter, ...dateFilters]
56
+ }
57
+
@@ -8,13 +8,20 @@ export const useWorkflowExecutionTableQuery = ({
8
8
  pageSize?: number
9
9
  prefix?: string
10
10
  }) => {
11
- const raw = useQueryParams(["q", "offset"], prefix)
11
+ const raw = useQueryParams(
12
+ ["q", "offset", "order", "state", "created_at", "updated_at"],
13
+ prefix
14
+ )
12
15
 
13
- const { offset, ...rest } = raw
16
+ const { offset, order, state, created_at, updated_at, ...rest } = raw
14
17
 
15
18
  const searchParams: HttpTypes.AdminGetWorkflowExecutionsParams = {
16
19
  limit: pageSize,
17
20
  offset: offset ? parseInt(offset) : 0,
21
+ order: order ?? "-created_at",
22
+ state: state ? state.split(",") : undefined,
23
+ created_at: created_at ? JSON.parse(created_at) : undefined,
24
+ updated_at: updated_at ? JSON.parse(updated_at) : undefined,
18
25
  ...rest,
19
26
  }
20
27
 
@@ -5,6 +5,7 @@ import { _DataTable } from "../../../../../components/table/data-table"
5
5
  import { useWorkflowExecutions } from "../../../../../hooks/api/workflow-executions"
6
6
  import { useDataTable } from "../../../../../hooks/use-data-table"
7
7
  import { useWorkflowExecutionTableColumns } from "./use-workflow-execution-table-columns"
8
+ import { useWorkflowExecutionTableFilters } from "./use-workflow-execution-table-filters"
8
9
  import { useWorkflowExecutionTableQuery } from "./use-workflow-execution-table-query"
9
10
 
10
11
  const PAGE_SIZE = 20
@@ -15,6 +16,7 @@ export const WorkflowExecutionListTable = () => {
15
16
  const { searchParams, raw } = useWorkflowExecutionTableQuery({
16
17
  pageSize: PAGE_SIZE,
17
18
  })
19
+
18
20
  const { workflow_executions, count, isLoading, isError, error } =
19
21
  useWorkflowExecutions(
20
22
  {
@@ -26,6 +28,7 @@ export const WorkflowExecutionListTable = () => {
26
28
  )
27
29
 
28
30
  const columns = useWorkflowExecutionTableColumns()
31
+ const filters = useWorkflowExecutionTableFilters()
29
32
 
30
33
  const { table } = useDataTable({
31
34
  data: workflow_executions || [],
@@ -53,8 +56,13 @@ export const WorkflowExecutionListTable = () => {
53
56
  <_DataTable
54
57
  table={table}
55
58
  columns={columns}
59
+ filters={filters}
56
60
  count={count}
57
61
  isLoading={isLoading}
62
+ orderBy={[
63
+ { key: "created_at", label: t("fields.createdAt") },
64
+ { key: "updated_at", label: t("fields.updatedAt") },
65
+ ]}
58
66
  pageSize={PAGE_SIZE}
59
67
  navigateTo={(row) => `${row.id}`}
60
68
  search
@@ -1,35 +0,0 @@
1
- // src/components/table/table-cells/common/status-cell/status-cell.tsx
2
- import { clx } from "@acmekit/ui";
3
- import { jsx, jsxs } from "react/jsx-runtime";
4
- var StatusCell = ({ color, children }) => {
5
- return /* @__PURE__ */ jsxs("div", { className: "txt-compact-small text-ui-fg-subtle flex h-full w-full items-center gap-x-2 overflow-hidden", children: [
6
- /* @__PURE__ */ jsx(
7
- "div",
8
- {
9
- role: "presentation",
10
- className: "flex h-5 w-2 items-center justify-center",
11
- children: /* @__PURE__ */ jsx(
12
- "div",
13
- {
14
- className: clx(
15
- "h-2 w-2 rounded-sm shadow-[0px_0px_0px_1px_rgba(0,0,0,0.12)_inset]",
16
- {
17
- "bg-ui-tag-neutral-icon": color === "grey",
18
- "bg-ui-tag-green-icon": color === "green",
19
- "bg-ui-tag-red-icon": color === "red",
20
- "bg-ui-tag-blue-icon": color === "blue",
21
- "bg-ui-tag-orange-icon": color === "orange",
22
- "bg-ui-tag-purple-icon": color === "purple"
23
- }
24
- )
25
- }
26
- )
27
- }
28
- ),
29
- /* @__PURE__ */ jsx("span", { className: "truncate", children })
30
- ] });
31
- };
32
-
33
- export {
34
- StatusCell
35
- };