@alepha/ui 0.16.1 → 0.17.0

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 (218) hide show
  1. package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CF_qOO3u.js} +20 -19
  2. package/dist/admin/AdminApiKeys-CF_qOO3u.js.map +1 -0
  3. package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BQno3hZG.js} +7 -7
  4. package/dist/admin/AdminAudits-BQno3hZG.js.map +1 -0
  5. package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-kvuUaASF.js} +3 -4
  6. package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-kvuUaASF.js.map} +1 -1
  7. package/dist/admin/AdminJobDashboard-CrPxp0W1.js +485 -0
  8. package/dist/admin/AdminJobDashboard-CrPxp0W1.js.map +1 -0
  9. package/dist/admin/AdminJobExecutions-D-b4Zt7W.js +678 -0
  10. package/dist/admin/AdminJobExecutions-D-b4Zt7W.js.map +1 -0
  11. package/dist/admin/AdminJobRegistry-CNX5cpDx.js +301 -0
  12. package/dist/admin/AdminJobRegistry-CNX5cpDx.js.map +1 -0
  13. package/dist/admin/{AdminLayout-BqZiXx4H.js → AdminLayout-e-ZP5nWw.js} +6 -9
  14. package/dist/admin/AdminLayout-e-ZP5nWw.js.map +1 -0
  15. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-DeHJFf6W.js} +3 -4
  16. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-DeHJFf6W.js.map} +1 -1
  17. package/dist/admin/AdminParameters-iQE8o7a7.js +774 -0
  18. package/dist/admin/AdminParameters-iQE8o7a7.js.map +1 -0
  19. package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-oKJCbd7w.js} +5 -6
  20. package/dist/admin/AdminSessions-oKJCbd7w.js.map +1 -0
  21. package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-BNCEle_E.js} +6 -7
  22. package/dist/admin/AdminUserAudits-BNCEle_E.js.map +1 -0
  23. package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CgqeFwCt.js} +6 -6
  24. package/dist/admin/AdminUserCreate-CgqeFwCt.js.map +1 -0
  25. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-DDe1A1GP.js} +30 -28
  26. package/dist/admin/AdminUserDetails-DDe1A1GP.js.map +1 -0
  27. package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-HAlobhWf.js} +20 -19
  28. package/dist/admin/AdminUserLayout-HAlobhWf.js.map +1 -0
  29. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Bq1LnVLf.js} +5 -6
  30. package/dist/admin/AdminUserSessions-Bq1LnVLf.js.map +1 -0
  31. package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-BRsBZoxV.js} +10 -9
  32. package/dist/admin/AdminUserSettings-BRsBZoxV.js.map +1 -0
  33. package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-D71kIOSn.js} +6 -7
  34. package/dist/admin/AdminUsers-D71kIOSn.js.map +1 -0
  35. package/dist/admin/index.d.ts +21 -85
  36. package/dist/admin/index.d.ts.map +1 -1
  37. package/dist/admin/index.js +66 -88
  38. package/dist/admin/index.js.map +1 -1
  39. package/dist/auth/{AuthLayout-Dj5K4SIN.js → AuthLayout-CdJcrPs4.js} +2 -3
  40. package/dist/auth/{AuthLayout-Dj5K4SIN.js.map → AuthLayout-CdJcrPs4.js.map} +1 -1
  41. package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
  42. package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
  43. package/dist/auth/{Login-BBqTosqZ.js → Login-BS_FYTy0.js} +19 -13
  44. package/dist/auth/Login-BS_FYTy0.js.map +1 -0
  45. package/dist/auth/{Profile-Bxj8Nwom.js → Profile-CjDsW378.js} +17 -12
  46. package/dist/auth/Profile-CjDsW378.js.map +1 -0
  47. package/dist/auth/{Register-Ce675Crg.js → Register-C5eqzAaD.js} +27 -17
  48. package/dist/auth/Register-C5eqzAaD.js.map +1 -0
  49. package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-XifinVao.js} +17 -10
  50. package/dist/auth/ResetPassword-XifinVao.js.map +1 -0
  51. package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DTgbeJOO.js} +9 -6
  52. package/dist/auth/VerifyEmail-DTgbeJOO.js.map +1 -0
  53. package/dist/auth/index.d.ts +18 -14
  54. package/dist/auth/index.d.ts.map +1 -1
  55. package/dist/auth/index.js +19 -18
  56. package/dist/auth/index.js.map +1 -1
  57. package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
  58. package/dist/core/index.d.ts +182 -92
  59. package/dist/core/index.d.ts.map +1 -1
  60. package/dist/core/index.js +789 -476
  61. package/dist/core/index.js.map +1 -1
  62. package/dist/demo/DemoDataTable-lnBKWBf8.js +362 -0
  63. package/dist/demo/DemoDataTable-lnBKWBf8.js.map +1 -0
  64. package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-CUMZsYaH.js} +6 -6
  65. package/dist/demo/DemoHome-CUMZsYaH.js.map +1 -0
  66. package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-_uokbGaW.js} +18 -19
  67. package/dist/demo/DemoJsonViewer-_uokbGaW.js.map +1 -0
  68. package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-DHVoacE6.js} +4 -5
  69. package/dist/demo/DemoLayout-DHVoacE6.js.map +1 -0
  70. package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DjJ9314c.js} +27 -24
  71. package/dist/demo/DemoLogin-DjJ9314c.js.map +1 -0
  72. package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DzkJ5M83.js} +39 -32
  73. package/dist/demo/DemoRegister-DzkJ5M83.js.map +1 -0
  74. package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-DWh4_BpQ.js} +30 -26
  75. package/dist/demo/DemoResetPassword-DWh4_BpQ.js.map +1 -0
  76. package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-C1csnGhX.js} +4 -5
  77. package/dist/demo/DemoSidebar-C1csnGhX.js.map +1 -0
  78. package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-CWz6fJrJ.js} +4 -5
  79. package/dist/demo/DemoTypeForm-CWz6fJrJ.js.map +1 -0
  80. package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-DbU_tCj8.js} +16 -16
  81. package/dist/demo/DemoVerifyEmail-DbU_tCj8.js.map +1 -0
  82. package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-Ch1m3Uzl.js} +2 -4
  83. package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-Ch1m3Uzl.js.map} +1 -1
  84. package/dist/demo/{Showcase-CQrMWars.js → Showcase-BzoXNlCn.js} +11 -13
  85. package/dist/demo/Showcase-BzoXNlCn.js.map +1 -0
  86. package/dist/demo/index.d.ts +3 -70
  87. package/dist/demo/index.d.ts.map +1 -1
  88. package/dist/demo/index.js +11 -15
  89. package/dist/demo/index.js.map +1 -1
  90. package/dist/json/index.js +2 -2
  91. package/dist/json/index.js.map +1 -1
  92. package/package.json +11 -5
  93. package/src/admin/AdminRouter.ts +51 -29
  94. package/src/admin/components/AdminLayout.tsx +6 -9
  95. package/src/admin/components/audits/AdminAudits.tsx +5 -5
  96. package/src/admin/components/jobs/AdminJobDashboard.tsx +455 -0
  97. package/src/admin/components/jobs/AdminJobExecutions.tsx +693 -0
  98. package/src/admin/components/jobs/AdminJobRegistry.tsx +325 -0
  99. package/src/admin/components/keys/AdminApiKeys.tsx +28 -31
  100. package/src/admin/components/parameters/AdminParameters.tsx +156 -78
  101. package/src/admin/components/parameters/ParameterDetails.tsx +173 -108
  102. package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
  103. package/src/admin/components/parameters/ParameterHistory.tsx +22 -35
  104. package/src/admin/components/parameters/ParameterTree.tsx +283 -109
  105. package/src/admin/components/parameters/types.ts +3 -3
  106. package/src/admin/components/sessions/AdminSessions.tsx +3 -3
  107. package/src/admin/components/shared/AdminResourceHeader.tsx +20 -16
  108. package/src/admin/components/users/AdminUserAudits.tsx +5 -5
  109. package/src/admin/components/users/AdminUserCreate.tsx +3 -3
  110. package/src/admin/components/users/AdminUserDetails.tsx +51 -53
  111. package/src/admin/components/users/AdminUserLayout.tsx +7 -7
  112. package/src/admin/components/users/AdminUserSessions.tsx +3 -3
  113. package/src/admin/components/users/AdminUserSettings.tsx +9 -9
  114. package/src/admin/components/users/AdminUsers.tsx +5 -5
  115. package/src/admin/components/verifications/AdminVerifications.tsx +3 -3
  116. package/src/admin/index.ts +0 -24
  117. package/src/admin/primitives/$uiAdmin.ts +2 -2
  118. package/src/auth/AuthRouter.ts +1 -0
  119. package/src/auth/components/Login.tsx +13 -13
  120. package/src/auth/components/Profile.tsx +17 -26
  121. package/src/auth/components/Register.tsx +21 -31
  122. package/src/auth/components/ResetPassword.tsx +13 -22
  123. package/src/auth/components/VerifyEmail.tsx +5 -5
  124. package/src/auth/components/buttons/UserButton.tsx +14 -4
  125. package/src/core/components/buttons/ActionButton.tsx +13 -17
  126. package/src/core/components/buttons/DarkModeButton.tsx +8 -4
  127. package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
  128. package/src/core/components/data/ErrorViewer.tsx +15 -15
  129. package/src/core/components/dialogs/AlertDialog.tsx +3 -3
  130. package/src/core/components/dialogs/ConfirmDialog.tsx +3 -3
  131. package/src/core/components/dialogs/PromptDialog.tsx +3 -3
  132. package/src/core/components/form/Control.tsx +19 -32
  133. package/src/core/components/form/ControlArray.tsx +206 -96
  134. package/src/core/components/form/ControlObject.tsx +3 -3
  135. package/src/core/components/form/ControlQueryBuilder.tsx +20 -22
  136. package/src/core/components/form/ControlSelect.tsx +4 -0
  137. package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
  138. package/src/core/components/form/TypeForm.tsx +7 -0
  139. package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
  140. package/src/core/components/layout/Breadcrumb.tsx +91 -0
  141. package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
  142. package/src/core/components/layout/Omnibar.tsx +2 -1
  143. package/src/core/components/layout/Sidebar.tsx +63 -19
  144. package/src/core/components/table/ColumnPicker.tsx +47 -31
  145. package/src/core/components/table/DataTable.tsx +277 -201
  146. package/src/core/components/table/DataTableFilters.tsx +8 -0
  147. package/src/core/components/table/DataTableToolbar.tsx +98 -5
  148. package/src/core/components/table/FilterPicker.tsx +28 -26
  149. package/src/core/components/table/types.ts +52 -37
  150. package/src/core/components/table/useTableSelection.ts +83 -0
  151. package/src/core/constants/ui.ts +1 -1
  152. package/src/core/helpers/renderIcon.tsx +5 -2
  153. package/src/core/index.ts +9 -5
  154. package/src/core/styles.css +8 -7
  155. package/src/core/utils/parseInput.ts +1 -0
  156. package/src/core/utils/string.ts +28 -4
  157. package/src/demo/components/DemoHome.tsx +5 -5
  158. package/src/demo/components/DemoLayout.tsx +6 -2
  159. package/src/demo/components/core/DemoDataTable.tsx +209 -5
  160. package/src/demo/components/json/DemoJsonViewer.tsx +1 -1
  161. package/src/demo/components/shared/MacWindow.tsx +7 -7
  162. package/src/demo/components/shared/Showcase.tsx +3 -3
  163. package/src/demo/index.ts +0 -11
  164. package/src/json/components/JsonViewer.tsx +3 -3
  165. package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
  166. package/dist/admin/AdminApiKeys-GMORg-1l.js.map +0 -1
  167. package/dist/admin/AdminAudits-8SM96viT.js +0 -3
  168. package/dist/admin/AdminAudits-pkWrjq1Z.js.map +0 -1
  169. package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
  170. package/dist/admin/AdminJobs-B-q9iGO3.js +0 -697
  171. package/dist/admin/AdminJobs-B-q9iGO3.js.map +0 -1
  172. package/dist/admin/AdminJobs-CED1syCn.js +0 -3
  173. package/dist/admin/AdminLayout-BqZiXx4H.js.map +0 -1
  174. package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
  175. package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
  176. package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
  177. package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
  178. package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
  179. package/dist/admin/AdminSessions-DzIOxM3b.js.map +0 -1
  180. package/dist/admin/AdminUserAudits-CiUPN2BC.js.map +0 -1
  181. package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
  182. package/dist/admin/AdminUserCreate-BwQKr4xE.js.map +0 -1
  183. package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
  184. package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
  185. package/dist/admin/AdminUserDetails-uqtC5aJ1.js.map +0 -1
  186. package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
  187. package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
  188. package/dist/admin/AdminUserSessions-DAE8Nf1F.js.map +0 -1
  189. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
  190. package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
  191. package/dist/admin/AdminUserSettings-EbahaV2a.js.map +0 -1
  192. package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
  193. package/dist/admin/AdminUsers-Dcjh0KNW.js.map +0 -1
  194. package/dist/auth/Login-BBqTosqZ.js.map +0 -1
  195. package/dist/auth/Login-CoU63mMR.js +0 -4
  196. package/dist/auth/Profile-Bxj8Nwom.js.map +0 -1
  197. package/dist/auth/Register-BV_oa_AK.js +0 -4
  198. package/dist/auth/Register-Ce675Crg.js.map +0 -1
  199. package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
  200. package/dist/auth/ResetPassword-DWdt7c40.js.map +0 -1
  201. package/dist/auth/VerifyEmail-CI4JwByV.js.map +0 -1
  202. package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
  203. package/dist/demo/DemoDataTable-CguplbR7.js +0 -150
  204. package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
  205. package/dist/demo/DemoHome-Cce2bWmg.js.map +0 -1
  206. package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
  207. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
  208. package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
  209. package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
  210. package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
  211. package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
  212. package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
  213. package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
  214. package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
  215. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
  216. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
  217. package/dist/demo/Showcase-CQrMWars.js.map +0 -1
  218. package/src/admin/components/jobs/AdminJobs.tsx +0 -772
@@ -0,0 +1,693 @@
1
+ import { ActionButton, DataTable, Text, useDialog, useToast } from "@alepha/ui";
2
+ import { Badge, Code, Flex, Paper, Table } from "@mantine/core";
3
+ import {
4
+ IconAlertTriangle,
5
+ IconCircleCheck,
6
+ IconCircleX,
7
+ IconClock,
8
+ IconPlayerPlay,
9
+ IconRefresh,
10
+ } from "@tabler/icons-react";
11
+ import { type Page, t } from "alepha";
12
+ import type {
13
+ AdminJobController,
14
+ JobExecutionDetailResource,
15
+ JobExecutionResource,
16
+ } from "alepha/api/jobs";
17
+ import type { LogEntry } from "alepha/logger";
18
+ import { useClient } from "alepha/react";
19
+ import { useI18n } from "alepha/react/i18n";
20
+ import { useCallback, useEffect, useState } from "react";
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+
24
+ const PRIORITY_LABELS: Record<number, string> = {
25
+ 0: "Critical",
26
+ 1: "High",
27
+ 2: "Normal",
28
+ 3: "Low",
29
+ };
30
+
31
+ const formatDuration = (
32
+ start: Date | string,
33
+ end?: Date | string | null,
34
+ ): string => {
35
+ const startTime = new Date(start).getTime();
36
+ const endTime = end ? new Date(end).getTime() : Date.now();
37
+ const duration = endTime - startTime;
38
+
39
+ if (duration < 1000) return `${duration}ms`;
40
+ if (duration < 60000) return `${(duration / 1000).toFixed(1)}s`;
41
+ if (duration < 3600000)
42
+ return `${Math.floor(duration / 60000)}m ${Math.floor((duration % 60000) / 1000)}s`;
43
+ return `${Math.floor(duration / 3600000)}h ${Math.floor((duration % 3600000) / 60000)}m`;
44
+ };
45
+
46
+ const getStatusColor = (status: string) => {
47
+ switch (status) {
48
+ case "completed":
49
+ return "teal";
50
+ case "running":
51
+ return "blue";
52
+ case "failed":
53
+ case "dead":
54
+ return "red";
55
+ case "cancelled":
56
+ return "orange";
57
+ case "scheduled":
58
+ return "violet";
59
+ case "retrying":
60
+ return "yellow";
61
+ case "pending":
62
+ return "gray";
63
+ default:
64
+ return "gray";
65
+ }
66
+ };
67
+
68
+ const getStatusIcon = (status: string, size = 14) => {
69
+ switch (status) {
70
+ case "completed":
71
+ return <IconCircleCheck size={size} />;
72
+ case "failed":
73
+ case "dead":
74
+ return <IconCircleX size={size} />;
75
+ case "running":
76
+ return <IconPlayerPlay size={size} />;
77
+ case "cancelled":
78
+ return <IconAlertTriangle size={size} />;
79
+ default:
80
+ return <IconClock size={size} />;
81
+ }
82
+ };
83
+
84
+ const getLogLevelColor = (level: string) => {
85
+ switch (level) {
86
+ case "ERROR":
87
+ return "red";
88
+ case "WARN":
89
+ return "orange";
90
+ case "INFO":
91
+ return "blue";
92
+ case "DEBUG":
93
+ return "gray";
94
+ case "TRACE":
95
+ return "dimmed";
96
+ default:
97
+ return "gray";
98
+ }
99
+ };
100
+
101
+ // ─────────────────────────────────────────────────────────────────────────────
102
+
103
+ const executionFilters = t.object({
104
+ job: t.optional(t.string()),
105
+ status: t.optional(
106
+ t.enum([
107
+ "pending",
108
+ "scheduled",
109
+ "retrying",
110
+ "running",
111
+ "completed",
112
+ "failed",
113
+ "dead",
114
+ "cancelled",
115
+ ]),
116
+ ),
117
+ priority: t.optional(t.enum(["critical", "high", "normal", "low"])),
118
+ });
119
+
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+
122
+ const AdminJobExecutions = () => {
123
+ const client = useClient<AdminJobController>();
124
+ const { l } = useI18n();
125
+ const toast = useToast();
126
+ const dialog = useDialog();
127
+ const [refreshKey, setRefreshKey] = useState(0);
128
+
129
+ const handleRetry = useCallback(
130
+ async (id: string) => {
131
+ try {
132
+ await client.retryExecution({ params: { id } });
133
+ toast.success("Execution retried");
134
+ setRefreshKey((k) => k + 1);
135
+ } catch {
136
+ toast.danger("Failed to retry execution");
137
+ }
138
+ },
139
+ [client, toast],
140
+ );
141
+
142
+ const handleCancel = useCallback(
143
+ async (id: string) => {
144
+ const confirmed = await dialog.confirm({
145
+ title: "Cancel Execution",
146
+ message: "Are you sure you want to cancel this execution?",
147
+ confirmLabel: "Cancel",
148
+ confirmColor: "red",
149
+ });
150
+
151
+ if (!confirmed) return;
152
+
153
+ try {
154
+ await client.cancelExecution({ params: { id } });
155
+ toast.success("Execution cancelled");
156
+ setRefreshKey((k) => k + 1);
157
+ } catch {
158
+ toast.danger("Failed to cancel execution");
159
+ }
160
+ },
161
+ [client, dialog, toast],
162
+ );
163
+
164
+ return (
165
+ <Flex flex={1} direction="column" gap="md">
166
+ <DataTable<JobExecutionResource, typeof executionFilters>
167
+ key={`executions-${refreshKey}`}
168
+ submitOnInit
169
+ defaultSize={20}
170
+ typeFormProps={{
171
+ skipSubmitButton: true,
172
+ columns: 3,
173
+ }}
174
+ tableProps={{
175
+ horizontalSpacing: "sm",
176
+ verticalSpacing: "sm",
177
+ highlightOnHover: true,
178
+ }}
179
+ onFilterChange={(key, _value, form) => {
180
+ if (key === "job" || key === "status" || key === "priority") {
181
+ return form.submit();
182
+ }
183
+ }}
184
+ filters={executionFilters}
185
+ defaultFilters={["job", "status"]}
186
+ items={async (filters) => {
187
+ const response = await client.findExecutions({
188
+ query: {
189
+ ...filters,
190
+ },
191
+ });
192
+ return response as Page<JobExecutionResource>;
193
+ }}
194
+ columns={{
195
+ status: {
196
+ label: "Status",
197
+ fit: true,
198
+ value: (item) => (
199
+ <Badge
200
+ size="sm"
201
+ variant="light"
202
+ color={getStatusColor(item.status)}
203
+ leftSection={getStatusIcon(item.status, 12)}
204
+ >
205
+ {item.status}
206
+ </Badge>
207
+ ),
208
+ },
209
+ jobName: {
210
+ label: "Job",
211
+ value: (item) => (
212
+ <Text size="sm" fw={500} ff="monospace">
213
+ {item.jobName}
214
+ </Text>
215
+ ),
216
+ },
217
+ priority: {
218
+ label: "Priority",
219
+ fit: true,
220
+ value: (item) => (
221
+ <Text size="xs" c="dimmed">
222
+ {PRIORITY_LABELS[item.priority] ?? item.priority}
223
+ </Text>
224
+ ),
225
+ },
226
+ attempt: {
227
+ label: "Attempt",
228
+ fit: true,
229
+ value: (item) => (
230
+ <Text size="sm" ff="monospace">
231
+ {item.attempt}/{item.maxAttempts}
232
+ </Text>
233
+ ),
234
+ },
235
+ triggeredByName: {
236
+ label: "Trigger",
237
+ fit: true,
238
+ defaultHidden: true,
239
+ value: (item) => (
240
+ <Text size="xs" c="dimmed">
241
+ {item.triggeredByName ?? "—"}
242
+ </Text>
243
+ ),
244
+ },
245
+ createdAt: {
246
+ label: "Created",
247
+ fit: true,
248
+ defaultHidden: true,
249
+ value: (item) => (
250
+ <Text size="xs" c="dimmed">
251
+ {l(item.createdAt, { date: "fromNow" })}
252
+ </Text>
253
+ ),
254
+ },
255
+ startedAt: {
256
+ label: "Started",
257
+ fit: true,
258
+ value: (item) => (
259
+ <Text size="xs" c="dimmed">
260
+ {item.startedAt ? l(item.startedAt, { date: "fromNow" }) : "—"}
261
+ </Text>
262
+ ),
263
+ },
264
+ duration: {
265
+ label: "Duration",
266
+ fit: true,
267
+ value: (item) => (
268
+ <Text size="xs" c="dimmed" ff="monospace">
269
+ {item.startedAt &&
270
+ (item.completedAt || item.status === "running")
271
+ ? formatDuration(item.startedAt, item.completedAt)
272
+ : "—"}
273
+ </Text>
274
+ ),
275
+ },
276
+ error: {
277
+ label: "Error",
278
+ defaultHidden: true,
279
+ value: (item) => (
280
+ <Text size="xs" c="red" lineClamp={1}>
281
+ {item.error ?? "—"}
282
+ </Text>
283
+ ),
284
+ },
285
+ key: {
286
+ label: "Key",
287
+ fit: true,
288
+ defaultHidden: true,
289
+ value: (item) => (
290
+ <Text size="xs" c="dimmed" ff="monospace">
291
+ {item.key ?? "—"}
292
+ </Text>
293
+ ),
294
+ },
295
+ workerId: {
296
+ label: "Worker",
297
+ fit: true,
298
+ defaultHidden: true,
299
+ value: (item) => (
300
+ <Text size="xs" c="dimmed" ff="monospace">
301
+ {item.workerId ?? "—"}
302
+ </Text>
303
+ ),
304
+ },
305
+ actions: {
306
+ label: "",
307
+ fit: true,
308
+ actions: (item) => [
309
+ {
310
+ tooltip: "Retry",
311
+ color: "blue",
312
+ icon: IconRefresh,
313
+ onClick: () => handleRetry(item.id),
314
+ visible: item.can?.retry,
315
+ },
316
+ {
317
+ tooltip: "Cancel",
318
+ color: "red",
319
+ icon: IconCircleX,
320
+ onClick: () => handleCancel(item.id),
321
+ visible: item.can?.cancel,
322
+ },
323
+ ],
324
+ },
325
+ }}
326
+ panel={{
327
+ can: (item) => Boolean(item.error || item.key || item.workerId),
328
+ render: (item) => (
329
+ <Flex direction="column" gap="sm" p="sm">
330
+ {item.error && (
331
+ <Flex direction="column" gap={2}>
332
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
333
+ Error
334
+ </Text>
335
+ <Paper p="xs" bg="var(--mantine-color-red-light)" radius="sm">
336
+ <Text
337
+ size="xs"
338
+ c="red"
339
+ style={{
340
+ whiteSpace: "pre-wrap",
341
+ wordBreak: "break-word",
342
+ }}
343
+ >
344
+ {item.error}
345
+ </Text>
346
+ </Paper>
347
+ </Flex>
348
+ )}
349
+ <Flex gap="lg" wrap="wrap">
350
+ <Flex direction="column" gap={2}>
351
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
352
+ ID
353
+ </Text>
354
+ <Text size="xs" ff="monospace">
355
+ {item.id}
356
+ </Text>
357
+ </Flex>
358
+ {item.key && (
359
+ <Flex direction="column" gap={2}>
360
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
361
+ Key
362
+ </Text>
363
+ <Text size="xs" ff="monospace">
364
+ {item.key}
365
+ </Text>
366
+ </Flex>
367
+ )}
368
+ {item.workerId && (
369
+ <Flex direction="column" gap={2}>
370
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
371
+ Worker
372
+ </Text>
373
+ <Text size="xs" ff="monospace">
374
+ {item.workerId}
375
+ </Text>
376
+ </Flex>
377
+ )}
378
+ {item.triggeredByName && (
379
+ <Flex direction="column" gap={2}>
380
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
381
+ Triggered By
382
+ </Text>
383
+ <Text size="xs">{item.triggeredByName}</Text>
384
+ </Flex>
385
+ )}
386
+ </Flex>
387
+ </Flex>
388
+ ),
389
+ }}
390
+ drawer={(item) => (
391
+ <ExecutionDetailContent
392
+ item={item}
393
+ onRetry={handleRetry}
394
+ onCancel={handleCancel}
395
+ />
396
+ )}
397
+ />
398
+ </Flex>
399
+ );
400
+ };
401
+
402
+ // ─────────────────────────────────────────────────────────────────────────────
403
+
404
+ const ExecutionDetailContent = ({
405
+ item,
406
+ onRetry,
407
+ onCancel,
408
+ }: {
409
+ item: JobExecutionResource;
410
+ onRetry: (id: string) => Promise<void>;
411
+ onCancel: (id: string) => Promise<void>;
412
+ }) => {
413
+ const client = useClient<AdminJobController>();
414
+ const { l } = useI18n();
415
+ const toast = useToast();
416
+ const [detail, setDetail] = useState<JobExecutionDetailResource | null>(null);
417
+ const [loading, setLoading] = useState(false);
418
+ const [expandedLogs, setExpandedLogs] = useState<Set<number>>(new Set());
419
+
420
+ const loadDetail = useCallback(
421
+ async (execId: string) => {
422
+ setDetail(null);
423
+ setExpandedLogs(new Set());
424
+ setLoading(true);
425
+ try {
426
+ const data = await client.getExecution({ params: { id: execId } });
427
+ setDetail(data);
428
+ } catch {
429
+ toast.danger("Failed to load execution details");
430
+ } finally {
431
+ setLoading(false);
432
+ }
433
+ },
434
+ [client, toast],
435
+ );
436
+
437
+ useEffect(() => {
438
+ loadDetail(item.id);
439
+ }, [item.id, loadDetail]);
440
+
441
+ const handleRetry = useCallback(async () => {
442
+ await onRetry(item.id);
443
+ loadDetail(item.id);
444
+ }, [item.id, onRetry, loadDetail]);
445
+
446
+ const handleCancel = useCallback(async () => {
447
+ await onCancel(item.id);
448
+ loadDetail(item.id);
449
+ }, [item.id, onCancel, loadDetail]);
450
+
451
+ const toggleLogExpand = useCallback((index: number) => {
452
+ setExpandedLogs((prev) => {
453
+ const next = new Set(prev);
454
+ if (next.has(index)) {
455
+ next.delete(index);
456
+ } else {
457
+ next.add(index);
458
+ }
459
+ return next;
460
+ });
461
+ }, []);
462
+
463
+ if (loading) {
464
+ return (
465
+ <Flex align="center" justify="center" py="xl">
466
+ <Text c="dimmed">Loading...</Text>
467
+ </Flex>
468
+ );
469
+ }
470
+
471
+ if (!detail) return null;
472
+
473
+ return (
474
+ <Flex direction="column" gap="md">
475
+ {/* Header */}
476
+ <Flex align="center" gap="sm">
477
+ <Text fw={600} ff="monospace">
478
+ {detail.jobName}
479
+ </Text>
480
+ <Badge
481
+ size="sm"
482
+ variant="light"
483
+ color={getStatusColor(detail.status)}
484
+ leftSection={getStatusIcon(detail.status, 12)}
485
+ >
486
+ {detail.status}
487
+ </Badge>
488
+ <Text size="xs" c="dimmed">
489
+ {detail.attempt}/{detail.maxAttempts}
490
+ </Text>
491
+ </Flex>
492
+
493
+ {/* Actions */}
494
+ <Flex gap="xs">
495
+ <ActionButton
496
+ tooltip="Refresh"
497
+ variant="light"
498
+ size="xs"
499
+ icon={IconRefresh}
500
+ onClick={() => loadDetail(item.id)}
501
+ />
502
+ {detail.can?.retry && (
503
+ <ActionButton
504
+ tooltip="Retry"
505
+ variant="light"
506
+ size="xs"
507
+ color="blue"
508
+ icon={IconRefresh}
509
+ onClick={handleRetry}
510
+ />
511
+ )}
512
+ {detail.can?.cancel && (
513
+ <ActionButton
514
+ tooltip="Cancel"
515
+ variant="light"
516
+ size="xs"
517
+ color="red"
518
+ icon={IconCircleX}
519
+ onClick={handleCancel}
520
+ />
521
+ )}
522
+ </Flex>
523
+
524
+ {/* Details */}
525
+ <Paper p="sm" radius="md" withBorder>
526
+ <Text size="sm" fw={600} mb="xs">
527
+ Details
528
+ </Text>
529
+ <Flex gap="lg" wrap="wrap">
530
+ <DetailField label="ID" value={detail.id} monospace copyable />
531
+ <DetailField label="Status" value={detail.status} capitalize />
532
+ <DetailField
533
+ label="Priority"
534
+ value={PRIORITY_LABELS[detail.priority] ?? "Normal"}
535
+ />
536
+ <DetailField
537
+ label="Attempt"
538
+ value={`${detail.attempt}/${detail.maxAttempts}`}
539
+ monospace
540
+ />
541
+ {detail.workerId && (
542
+ <DetailField label="Worker" value={detail.workerId} monospace />
543
+ )}
544
+ {detail.key && (
545
+ <DetailField label="Key" value={detail.key} monospace />
546
+ )}
547
+ <DetailField
548
+ label="Created"
549
+ value={String(l(detail.createdAt, { date: "lll" }))}
550
+ />
551
+ {detail.startedAt && (
552
+ <DetailField
553
+ label="Started"
554
+ value={String(l(detail.startedAt, { date: "lll" }))}
555
+ />
556
+ )}
557
+ {detail.startedAt &&
558
+ (detail.completedAt || detail.status === "running") && (
559
+ <DetailField
560
+ label="Duration"
561
+ value={formatDuration(detail.startedAt, detail.completedAt)}
562
+ monospace
563
+ />
564
+ )}
565
+ {detail.triggeredByName && (
566
+ <DetailField label="Triggered By" value={detail.triggeredByName} />
567
+ )}
568
+ {detail.cancelledByName && (
569
+ <DetailField label="Cancelled By" value={detail.cancelledByName} />
570
+ )}
571
+ </Flex>
572
+ </Paper>
573
+
574
+ {/* Payload */}
575
+ {detail.payload && (
576
+ <Paper p="sm" radius="md" withBorder>
577
+ <Text size="sm" fw={600} mb="xs">
578
+ Payload
579
+ </Text>
580
+ <Code block>{JSON.stringify(detail.payload, null, 2)}</Code>
581
+ </Paper>
582
+ )}
583
+
584
+ {/* Error */}
585
+ {detail.error && (
586
+ <Paper p="sm" radius="md" withBorder>
587
+ <Text size="sm" fw={600} mb="xs" c="red">
588
+ Error
589
+ </Text>
590
+ <Paper p="xs" bg="var(--mantine-color-red-light)" radius="sm">
591
+ <Text
592
+ size="sm"
593
+ c="red"
594
+ style={{
595
+ whiteSpace: "pre-wrap",
596
+ wordBreak: "break-word",
597
+ }}
598
+ >
599
+ {detail.error}
600
+ </Text>
601
+ </Paper>
602
+ </Paper>
603
+ )}
604
+
605
+ {/* Logs */}
606
+ {detail.logs && detail.logs.length > 0 && (
607
+ <Paper p="sm" radius="md" withBorder>
608
+ <Text size="sm" fw={600} mb="xs">
609
+ Logs ({detail.logs.length})
610
+ </Text>
611
+ <Flex
612
+ direction="column"
613
+ style={{ maxHeight: 400, overflowY: "auto" }}
614
+ >
615
+ <Table highlightOnHover>
616
+ <Table.Thead>
617
+ <Table.Tr>
618
+ <Table.Th style={{ width: 60 }}>Level</Table.Th>
619
+ <Table.Th style={{ width: 90 }}>Time</Table.Th>
620
+ <Table.Th>Message</Table.Th>
621
+ </Table.Tr>
622
+ </Table.Thead>
623
+ <Table.Tbody>
624
+ {detail.logs.map((log: LogEntry, i: number) => (
625
+ <Table.Tr
626
+ key={i}
627
+ style={log.data ? { cursor: "pointer" } : undefined}
628
+ onClick={log.data ? () => toggleLogExpand(i) : undefined}
629
+ >
630
+ <Table.Td>
631
+ <Badge
632
+ size="xs"
633
+ variant="light"
634
+ color={getLogLevelColor(log.level)}
635
+ >
636
+ {log.level}
637
+ </Badge>
638
+ </Table.Td>
639
+ <Table.Td>
640
+ <Text size="xs" c="dimmed" ff="monospace">
641
+ {new Date(log.timestamp).toLocaleTimeString()}
642
+ </Text>
643
+ </Table.Td>
644
+ <Table.Td>
645
+ <Text size="xs">{log.message}</Text>
646
+ {expandedLogs.has(i) && log.data && (
647
+ <Code block mt="xs">
648
+ {JSON.stringify(log.data, null, 2)}
649
+ </Code>
650
+ )}
651
+ </Table.Td>
652
+ </Table.Tr>
653
+ ))}
654
+ </Table.Tbody>
655
+ </Table>
656
+ </Flex>
657
+ </Paper>
658
+ )}
659
+ </Flex>
660
+ );
661
+ };
662
+
663
+ // ─────────────────────────────────────────────────────────────────────────────
664
+
665
+ const DetailField = ({
666
+ label,
667
+ value,
668
+ monospace,
669
+ capitalize,
670
+ copyable,
671
+ }: {
672
+ label: string;
673
+ value: string;
674
+ monospace?: boolean;
675
+ capitalize?: boolean;
676
+ copyable?: boolean;
677
+ }) => (
678
+ <Flex direction="column" gap={2}>
679
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
680
+ {label}
681
+ </Text>
682
+ <Text
683
+ size="sm"
684
+ ff={monospace ? "monospace" : undefined}
685
+ tt={capitalize ? "capitalize" : undefined}
686
+ style={copyable ? { cursor: "pointer", userSelect: "all" } : undefined}
687
+ >
688
+ {value}
689
+ </Text>
690
+ </Flex>
691
+ );
692
+
693
+ export default AdminJobExecutions;