@alepha/ui 0.18.3 → 0.19.1

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 (181) hide show
  1. package/dist/admin/{AdminApiKeys-Dy_k-4Vd.js → AdminApiKeys-C2ze85eD.js} +3 -4
  2. package/dist/admin/{AdminApiKeys-Dy_k-4Vd.js.map → AdminApiKeys-C2ze85eD.js.map} +1 -1
  3. package/dist/admin/{AdminAudits-CKiFMSSU.js → AdminAudits-BIj81e4k.js} +3 -4
  4. package/dist/admin/{AdminAudits-CKiFMSSU.js.map → AdminAudits-BIj81e4k.js.map} +1 -1
  5. package/dist/admin/{AdminDashboard-PhC_dZqo.js → AdminDashboard-PMVzrwSu.js} +3 -4
  6. package/dist/admin/{AdminDashboard-PhC_dZqo.js.map → AdminDashboard-PMVzrwSu.js.map} +1 -1
  7. package/dist/admin/AdminFiles-Bq03BLt-.js +189 -0
  8. package/dist/admin/AdminFiles-Bq03BLt-.js.map +1 -0
  9. package/dist/admin/{AdminJobExecutions-D9E-CS-U.js → AdminJobs-D1_QGCDy.js} +401 -358
  10. package/dist/admin/AdminJobs-D1_QGCDy.js.map +1 -0
  11. package/dist/admin/{AdminLayout-I6TlUMPc.js → AdminLayout-BNiwiw2D.js} +8 -25
  12. package/dist/admin/AdminLayout-BNiwiw2D.js.map +1 -0
  13. package/dist/admin/{AdminNotifications-ZPHCYrv7.js → AdminNotifications-DSKQtUfn.js} +85 -124
  14. package/dist/admin/AdminNotifications-DSKQtUfn.js.map +1 -0
  15. package/dist/admin/{AdminParameters-CqgvhRsb.js → AdminParameters-CoB7EhyM.js} +3 -12
  16. package/dist/admin/{AdminParameters-CqgvhRsb.js.map → AdminParameters-CoB7EhyM.js.map} +1 -1
  17. package/dist/admin/{AdminSessions-Bz5NRuoW.js → AdminSessions-DFbFcrJQ.js} +3 -4
  18. package/dist/admin/{AdminSessions-Bz5NRuoW.js.map → AdminSessions-DFbFcrJQ.js.map} +1 -1
  19. package/dist/admin/{AdminUserLayout-lXT6I0Qq.js → AdminUserLayout-fSfi3KMm.js} +72 -111
  20. package/dist/admin/AdminUserLayout-fSfi3KMm.js.map +1 -0
  21. package/dist/admin/{AdminUserProfile-vFBLoJ3h.js → AdminUserProfile-_C-h8vUK.js} +7 -6
  22. package/dist/admin/AdminUserProfile-_C-h8vUK.js.map +1 -0
  23. package/dist/admin/{AdminUserSessions-CT_YDim0.js → AdminUserSessions-KpJHIeQo.js} +3 -4
  24. package/dist/admin/{AdminUserSessions-CT_YDim0.js.map → AdminUserSessions-KpJHIeQo.js.map} +1 -1
  25. package/dist/admin/{AdminUsers-D1UfGya9.js → AdminUsers-DcVrzdQP.js} +4 -4
  26. package/dist/admin/AdminUsers-DcVrzdQP.js.map +1 -0
  27. package/dist/admin/{AuthLayout-_frhdgOO.js → AuthLayout-CazfLzcf.js} +3 -4
  28. package/dist/admin/{AuthLayout-_frhdgOO.js.map → AuthLayout-CazfLzcf.js.map} +1 -1
  29. package/dist/{demo/IconGoogle-CSQLPYwX.js → admin/IconGoogle-8Nkx6yax.js} +2 -4
  30. package/dist/admin/{IconGoogle-Ch1m3Uzl.js.map → IconGoogle-8Nkx6yax.js.map} +1 -1
  31. package/dist/admin/{Login-xtNmQtGh.js → Login-CaMjUrDP.js} +5 -6
  32. package/dist/{auth/Login-BA1E8IZl.js.map → admin/Login-CaMjUrDP.js.map} +1 -1
  33. package/dist/admin/{Profile-_AtPUwAP.js → Profile-Ca4fZX15.js} +3 -5
  34. package/dist/{demo/Profile-DS5q4vOh.js.map → admin/Profile-Ca4fZX15.js.map} +1 -1
  35. package/dist/admin/{Register-JcCjHUUn.js → Register-C5DyKWPO.js} +5 -6
  36. package/dist/{demo/Register-B4hLBeEv.js.map → admin/Register-C5DyKWPO.js.map} +1 -1
  37. package/dist/admin/{ResetPassword-CwGBPLJO.js → ResetPassword-BA5sAgXo.js} +4 -5
  38. package/dist/{auth/ResetPassword-DCtGcneA.js.map → admin/ResetPassword-BA5sAgXo.js.map} +1 -1
  39. package/dist/admin/{VerifyEmail-hNxWejWf.js → VerifyEmail-DKNXROj_.js} +4 -5
  40. package/dist/{auth/VerifyEmail-DkH7NBfn.js.map → admin/VerifyEmail-DKNXROj_.js.map} +1 -1
  41. package/dist/admin/adminUserAtom-BLNc7XbT.js +11 -0
  42. package/dist/admin/adminUserAtom-BLNc7XbT.js.map +1 -0
  43. package/dist/admin/{core-CYaRQ8O-.js → core-CJCEx18C.js} +132 -86
  44. package/dist/admin/core-CJCEx18C.js.map +1 -0
  45. package/dist/admin/index.d.ts +80 -13
  46. package/dist/admin/index.d.ts.map +1 -1
  47. package/dist/admin/index.js +38 -68
  48. package/dist/admin/index.js.map +1 -1
  49. package/dist/admin/rolldown-runtime-CiIaOW0V.js +13 -0
  50. package/dist/{demo/AuthLayout-Brri4A-L.js → auth/AuthLayout-vXPcCVzp.js} +3 -4
  51. package/dist/auth/{AuthLayout-AvLlcLjS.js.map → AuthLayout-vXPcCVzp.js.map} +1 -1
  52. package/dist/{admin/IconGoogle-Ch1m3Uzl.js → auth/IconGoogle-8Nkx6yax.js} +2 -4
  53. package/dist/auth/{IconGoogle-Ch1m3Uzl.js.map → IconGoogle-8Nkx6yax.js.map} +1 -1
  54. package/dist/auth/{Login-BA1E8IZl.js → Login-Dg08QR20.js} +5 -6
  55. package/dist/{demo/Login-C12N4oGs.js.map → auth/Login-Dg08QR20.js.map} +1 -1
  56. package/dist/{demo/Profile-DS5q4vOh.js → auth/Profile-Bb5O1yeh.js} +3 -5
  57. package/dist/auth/{Profile-YcWdeuFz.js.map → Profile-Bb5O1yeh.js.map} +1 -1
  58. package/dist/auth/{Register-CPhEO5MG.js → Register-B2AN71NC.js} +5 -6
  59. package/dist/{admin/Register-JcCjHUUn.js.map → auth/Register-B2AN71NC.js.map} +1 -1
  60. package/dist/{demo/ResetPassword-D8g9ha1N.js → auth/ResetPassword-BLxwzbDj.js} +4 -5
  61. package/dist/{admin/ResetPassword-CwGBPLJO.js.map → auth/ResetPassword-BLxwzbDj.js.map} +1 -1
  62. package/dist/auth/{VerifyEmail-DkH7NBfn.js → VerifyEmail-CSDOk3Zm.js} +4 -5
  63. package/dist/{admin/VerifyEmail-hNxWejWf.js.map → auth/VerifyEmail-CSDOk3Zm.js.map} +1 -1
  64. package/dist/auth/{core-D5jIAVF2.js → core-DuGkjPiU.js} +23 -54
  65. package/dist/auth/core-DuGkjPiU.js.map +1 -0
  66. package/dist/auth/index.d.ts +20 -6
  67. package/dist/auth/index.d.ts.map +1 -1
  68. package/dist/auth/index.js +13 -18
  69. package/dist/auth/index.js.map +1 -1
  70. package/dist/auth/rolldown-runtime-CiIaOW0V.js +13 -0
  71. package/dist/core/index.d.ts +78 -20
  72. package/dist/core/index.d.ts.map +1 -1
  73. package/dist/core/index.js +130 -98
  74. package/dist/core/index.js.map +1 -1
  75. package/dist/{auth/AuthLayout-AvLlcLjS.js → demo/AuthLayout-DPsOOG4u.js} +3 -4
  76. package/dist/demo/{AuthLayout-Brri4A-L.js.map → AuthLayout-DPsOOG4u.js.map} +1 -1
  77. package/dist/demo/{DemoButton-wiCxZZ_L.js → DemoButton-wzcqGk4u.js} +4 -5
  78. package/dist/demo/{DemoButton-wiCxZZ_L.js.map → DemoButton-wzcqGk4u.js.map} +1 -1
  79. package/dist/demo/{DemoControlSelect-D7ILObVg.js → DemoControlSelect-CMWvQ6Gm.js} +4 -5
  80. package/dist/demo/{DemoControlSelect-D7ILObVg.js.map → DemoControlSelect-CMWvQ6Gm.js.map} +1 -1
  81. package/dist/demo/{DemoDataTable-DZ5Y8pFX.js → DemoDataTable-CHsAP3e2.js} +4 -5
  82. package/dist/demo/{DemoDataTable-DZ5Y8pFX.js.map → DemoDataTable-CHsAP3e2.js.map} +1 -1
  83. package/dist/demo/{DemoDialog-CUWdLHim.js → DemoDialog-Co2IePxX.js} +3 -4
  84. package/dist/demo/{DemoDialog-CUWdLHim.js.map → DemoDialog-Co2IePxX.js.map} +1 -1
  85. package/dist/demo/{DemoFlex-a8OhMMvq.js → DemoFlex-OEwQt5do.js} +4 -5
  86. package/dist/demo/{DemoFlex-a8OhMMvq.js.map → DemoFlex-OEwQt5do.js.map} +1 -1
  87. package/dist/demo/DemoHeading-Db-XkQIK.js +69 -0
  88. package/dist/demo/DemoHeading-Db-XkQIK.js.map +1 -0
  89. package/dist/demo/{DemoHome-D_De3UiT.js → DemoHome-Cyp29ygy.js} +4 -5
  90. package/dist/demo/{DemoHome-D_De3UiT.js.map → DemoHome-Cyp29ygy.js.map} +1 -1
  91. package/dist/demo/{DemoJsonViewer-B50s9aGM.js → DemoJsonViewer-DXtCeMzH.js} +4 -5
  92. package/dist/demo/{DemoJsonViewer-B50s9aGM.js.map → DemoJsonViewer-DXtCeMzH.js.map} +1 -1
  93. package/dist/demo/{DemoLayout-CHU8WTwO.js → DemoLayout-hh9VmZQP.js} +4 -5
  94. package/dist/demo/DemoLayout-hh9VmZQP.js.map +1 -0
  95. package/dist/demo/{DemoLogin-BBlrWpml.js → DemoLogin-DX7mnmkh.js} +15 -11
  96. package/dist/demo/{DemoLogin-BBlrWpml.js.map → DemoLogin-DX7mnmkh.js.map} +1 -1
  97. package/dist/demo/{DemoRegister-BuNE3_-f.js → DemoRegister-DVcZl04m.js} +15 -11
  98. package/dist/demo/{DemoRegister-BuNE3_-f.js.map → DemoRegister-DVcZl04m.js.map} +1 -1
  99. package/dist/demo/{DemoResetPassword-D_IjjjOJ.js → DemoResetPassword-CPENlZH5.js} +15 -11
  100. package/dist/demo/{DemoResetPassword-D_IjjjOJ.js.map → DemoResetPassword-CPENlZH5.js.map} +1 -1
  101. package/dist/demo/{DemoSidebar-Giy2HRBD.js → DemoSidebar-CGu7DZeM.js} +4 -5
  102. package/dist/demo/{DemoSidebar-Giy2HRBD.js.map → DemoSidebar-CGu7DZeM.js.map} +1 -1
  103. package/dist/demo/{DemoText-ubcw-vog.js → DemoText-DYUJ7bY_.js} +4 -5
  104. package/dist/demo/{DemoText-ubcw-vog.js.map → DemoText-DYUJ7bY_.js.map} +1 -1
  105. package/dist/demo/{DemoToast-9die_dYT.js → DemoToast-CgdnZNvx.js} +3 -4
  106. package/dist/demo/{DemoToast-9die_dYT.js.map → DemoToast-CgdnZNvx.js.map} +1 -1
  107. package/dist/demo/{DemoTypeForm-D_d6OVKL.js → DemoTypeForm-Pims-cGa.js} +4 -5
  108. package/dist/demo/{DemoTypeForm-D_d6OVKL.js.map → DemoTypeForm-Pims-cGa.js.map} +1 -1
  109. package/dist/demo/{DemoVerifyEmail-B43KlF4F.js → DemoVerifyEmail-C7B3xxch.js} +10 -11
  110. package/dist/demo/{DemoVerifyEmail-B43KlF4F.js.map → DemoVerifyEmail-C7B3xxch.js.map} +1 -1
  111. package/dist/{auth/IconGoogle-Ch1m3Uzl.js → demo/IconGoogle-CwQy4G9y.js} +2 -4
  112. package/dist/demo/{IconGoogle-CSQLPYwX.js.map → IconGoogle-CwQy4G9y.js.map} +1 -1
  113. package/dist/demo/{Login-C12N4oGs.js → Login-pwMF4TUj.js} +5 -6
  114. package/dist/{admin/Login-xtNmQtGh.js.map → demo/Login-pwMF4TUj.js.map} +1 -1
  115. package/dist/{auth/Profile-YcWdeuFz.js → demo/Profile-BliZapZS.js} +3 -5
  116. package/dist/{admin/Profile-_AtPUwAP.js.map → demo/Profile-BliZapZS.js.map} +1 -1
  117. package/dist/demo/{Register-B4hLBeEv.js → Register-CiwAT7Hy.js} +5 -6
  118. package/dist/{auth/Register-CPhEO5MG.js.map → demo/Register-CiwAT7Hy.js.map} +1 -1
  119. package/dist/{auth/ResetPassword-DCtGcneA.js → demo/ResetPassword-l9Vg4JE-.js} +4 -5
  120. package/dist/demo/{ResetPassword-D8g9ha1N.js.map → ResetPassword-l9Vg4JE-.js.map} +1 -1
  121. package/dist/demo/{Showcase-D6Fxt4X4.js → Showcase-CX6bDgwe.js} +3 -5
  122. package/dist/demo/{Showcase-D6Fxt4X4.js.map → Showcase-CX6bDgwe.js.map} +1 -1
  123. package/dist/demo/{VerifyEmail-BjDo0cZA.js → VerifyEmail-CAB-OS7i.js} +4 -5
  124. package/dist/demo/{VerifyEmail-BjDo0cZA.js.map → VerifyEmail-CAB-OS7i.js.map} +1 -1
  125. package/dist/demo/{auth-ByVTreDl.js → auth-uegJAdKu.js} +18 -35
  126. package/dist/demo/{auth-ByVTreDl.js.map → auth-uegJAdKu.js.map} +1 -1
  127. package/dist/demo/{core-DFgB3yU4.js → core-B4LVHzPn.js} +132 -93
  128. package/dist/demo/core-B4LVHzPn.js.map +1 -0
  129. package/dist/demo/index.js +20 -23
  130. package/dist/demo/index.js.map +1 -1
  131. package/dist/demo/rolldown-runtime-CiIaOW0V.js +13 -0
  132. package/package.json +17 -20
  133. package/src/admin/AdminRouter.tsx +23 -38
  134. package/src/admin/atoms/adminUserAtom.ts +7 -0
  135. package/src/admin/components/AdminLayout.tsx +2 -14
  136. package/src/admin/components/files/AdminFiles.tsx +123 -1
  137. package/src/admin/components/jobs/{AdminJobExecutions.tsx → AdminJobs.tsx} +450 -317
  138. package/src/admin/components/notifications/AdminNotifications.tsx +11 -25
  139. package/src/admin/components/users/AdminUserLayout.tsx +84 -127
  140. package/src/admin/components/users/AdminUserProfile.tsx +5 -2
  141. package/src/admin/components/users/AdminUsers.tsx +1 -1
  142. package/src/core/components/Flex.tsx +24 -0
  143. package/src/core/components/Section.tsx +109 -0
  144. package/src/core/components/SectionHeader.tsx +106 -0
  145. package/src/core/components/buttons/ActionButton.tsx +1 -0
  146. package/src/core/components/dialogs/PromptDialog.tsx +1 -1
  147. package/src/core/components/layout/Breadcrumb.tsx +2 -2
  148. package/src/core/components/layout/DashboardShell.tsx +1 -1
  149. package/src/core/index.ts +4 -1
  150. package/src/core/services/DialogService.tsx +2 -2
  151. package/src/core/styles.css +2 -1
  152. package/src/core/table/components/DataTable.tsx +5 -2
  153. package/src/demo/DemoRouter.ts +1 -1
  154. package/src/demo/components/auth/DemoLogin.tsx +5 -0
  155. package/src/demo/components/auth/DemoRegister.tsx +5 -0
  156. package/src/demo/components/auth/DemoResetPassword.tsx +5 -0
  157. package/src/demo/components/core/DemoHeading.tsx +56 -3
  158. package/dist/admin/AdminFiles-DFTjijGp.js +0 -111
  159. package/dist/admin/AdminFiles-DFTjijGp.js.map +0 -1
  160. package/dist/admin/AdminJobDashboard-BL8gGPDp.js +0 -354
  161. package/dist/admin/AdminJobDashboard-BL8gGPDp.js.map +0 -1
  162. package/dist/admin/AdminJobExecutions-D9E-CS-U.js.map +0 -1
  163. package/dist/admin/AdminJobRegistry-Ci9ue1zC.js +0 -270
  164. package/dist/admin/AdminJobRegistry-Ci9ue1zC.js.map +0 -1
  165. package/dist/admin/AdminLayout-I6TlUMPc.js.map +0 -1
  166. package/dist/admin/AdminNotifications-ZPHCYrv7.js.map +0 -1
  167. package/dist/admin/AdminUserLayout-lXT6I0Qq.js.map +0 -1
  168. package/dist/admin/AdminUserProfile-vFBLoJ3h.js.map +0 -1
  169. package/dist/admin/AdminUsers-D1UfGya9.js.map +0 -1
  170. package/dist/admin/core-CYaRQ8O-.js.map +0 -1
  171. package/dist/admin/rolldown-runtime-CjeV3_4I.js +0 -18
  172. package/dist/auth/core-D5jIAVF2.js.map +0 -1
  173. package/dist/auth/rolldown-runtime-CjeV3_4I.js +0 -18
  174. package/dist/demo/DemoHeading-C13OVDfS.js +0 -18
  175. package/dist/demo/DemoHeading-C13OVDfS.js.map +0 -1
  176. package/dist/demo/DemoLayout-CHU8WTwO.js.map +0 -1
  177. package/dist/demo/core-DFgB3yU4.js.map +0 -1
  178. package/dist/demo/rolldown-runtime-CjeV3_4I.js +0 -18
  179. package/src/admin/components/jobs/AdminJobDashboard.tsx +0 -349
  180. package/src/admin/components/jobs/AdminJobRegistry.tsx +0 -301
  181. package/src/core/components/Heading.tsx +0 -19
@@ -4,17 +4,27 @@ import {
4
4
  DataTable,
5
5
  DetailList,
6
6
  Flex,
7
+ Section,
7
8
  Text,
8
9
  useDialog,
9
10
  useToast,
10
11
  } from "@alepha/ui";
11
- import { Badge, Code, Paper, Table } from "@mantine/core";
12
- import { IconCircleX, IconRefresh } from "@tabler/icons-react";
12
+ import { Badge, Code, Table } from "@mantine/core";
13
+ import {
14
+ IconCircleCheck,
15
+ IconCircleX,
16
+ IconPlayerPlay,
17
+ IconRefresh,
18
+ } from "@tabler/icons-react";
13
19
  import { type Page, t } from "alepha";
14
20
  import type {
15
21
  AdminJobController,
22
+ JobCronInfo,
16
23
  JobExecutionDetailResource,
17
24
  JobExecutionResource,
25
+ JobFailure,
26
+ JobQueueDepth,
27
+ JobRegistration,
18
28
  } from "alepha/api/jobs";
19
29
  import type { LogEntry } from "alepha/logger";
20
30
  import { useClient } from "alepha/react";
@@ -47,293 +57,14 @@ const formatDuration = (
47
57
 
48
58
  // ─────────────────────────────────────────────────────────────────────────────
49
59
 
50
- const executionFilters = t.object({
51
- job: t.optional(t.string()),
52
- status: t.optional(
53
- t.enum([
54
- "pending",
55
- "scheduled",
56
- "retrying",
57
- "running",
58
- "completed",
59
- "failed",
60
- "dead",
61
- "cancelled",
62
- ]),
63
- ),
64
- priority: t.optional(t.enum(["critical", "high", "normal", "low"])),
60
+ const registryFilters = t.object({
61
+ type: t.optional(t.enum(["cron", "push", "both"])),
65
62
  });
66
63
 
67
- // ─────────────────────────────────────────────────────────────────────────────
68
-
69
- const AdminJobExecutions = () => {
70
- const client = useClient<AdminJobController>();
71
- const { l } = useI18n();
72
- const toast = useToast();
73
- const dialog = useDialog();
74
- const [refreshKey, setRefreshKey] = useState(0);
75
-
76
- const handleRetry = useCallback(
77
- async (id: string) => {
78
- try {
79
- await client.retryJobExecution({ params: { id } });
80
- toast.success("Execution retried");
81
- setRefreshKey((k) => k + 1);
82
- } catch {
83
- toast.danger("Failed to retry execution");
84
- }
85
- },
86
- [client, toast],
87
- );
88
-
89
- const handleCancel = useCallback(
90
- async (id: string) => {
91
- const confirmed = await dialog.confirm({
92
- title: "Cancel Execution",
93
- message: "Are you sure you want to cancel this execution?",
94
- confirmLabel: "Cancel",
95
- confirmColor: "red",
96
- });
97
-
98
- if (!confirmed) return;
99
-
100
- try {
101
- await client.cancelJobExecution({ params: { id } });
102
- toast.success("Execution cancelled");
103
- setRefreshKey((k) => k + 1);
104
- } catch {
105
- toast.danger("Failed to cancel execution");
106
- }
107
- },
108
- [client, dialog, toast],
109
- );
110
-
111
- return (
112
- <Flex p="md" flex={1} direction="column" gap="md">
113
- <DataTable<JobExecutionResource, typeof executionFilters>
114
- key={`executions-${refreshKey}`}
115
- submitOnInit
116
- defaultSize={20}
117
- typeFormProps={{
118
- skipSubmitButton: true,
119
- columns: 3,
120
- }}
121
- tableProps={{
122
- horizontalSpacing: "sm",
123
- verticalSpacing: "sm",
124
- }}
125
- onFilterChange={(_key, _value, form) => form.submit()}
126
- filters={executionFilters}
127
- defaultFilters={["job", "status"]}
128
- items={async (filters) => {
129
- const response = await client.findJobExecutions({
130
- query: {
131
- ...filters,
132
- },
133
- });
134
- return response as Page<JobExecutionResource>;
135
- }}
136
- columns={{
137
- status: {
138
- label: "Status",
139
- value: (item) => {
140
- const color =
141
- item.status === "completed"
142
- ? "green"
143
- : item.status === "running"
144
- ? "blue"
145
- : item.status === "failed" || item.status === "dead"
146
- ? "red"
147
- : item.status === "cancelled"
148
- ? "yellow"
149
- : "gray";
150
- return (
151
- <Badge size="sm" variant="light" color={color}>
152
- {item.status}
153
- </Badge>
154
- );
155
- },
156
- },
157
- jobName: {
158
- label: "Job",
159
- value: (item) => (
160
- <Text size="sm" fw={500} ff="monospace">
161
- {item.jobName}
162
- </Text>
163
- ),
164
- },
165
- priority: {
166
- label: "Priority",
167
- value: (item) => (
168
- <Text size="xs" c="dimmed">
169
- {PRIORITY_LABELS[item.priority] ?? item.priority}
170
- </Text>
171
- ),
172
- },
173
- attempt: {
174
- label: "Attempt",
175
- value: (item) => (
176
- <Text size="sm" ff="monospace">
177
- {item.attempt}/{item.maxAttempts}
178
- </Text>
179
- ),
180
- },
181
- triggeredByName: {
182
- label: "Trigger",
183
- defaultHidden: true,
184
- value: (item) => (
185
- <Text size="xs" c="dimmed">
186
- {item.triggeredByName ?? "\u2014"}
187
- </Text>
188
- ),
189
- },
190
- createdAt: {
191
- label: "Created",
192
- defaultHidden: true,
193
- value: (item) => (
194
- <Text size="xs" c="dimmed">
195
- {l(item.createdAt, { date: "fromNow" })}
196
- </Text>
197
- ),
198
- },
199
- startedAt: {
200
- label: "Started",
201
- value: (item) => (
202
- <Text size="xs" c="dimmed">
203
- {item.startedAt
204
- ? l(item.startedAt, { date: "fromNow" })
205
- : "\u2014"}
206
- </Text>
207
- ),
208
- },
209
- duration: {
210
- label: "Duration",
211
- value: (item) => (
212
- <Text size="xs" c="dimmed" ff="monospace">
213
- {item.startedAt &&
214
- (item.completedAt || item.status === "running")
215
- ? formatDuration(item.startedAt, item.completedAt)
216
- : "\u2014"}
217
- </Text>
218
- ),
219
- },
220
- error: {
221
- label: "Error",
222
- defaultHidden: true,
223
- value: (item) => (
224
- <Text size="xs" c="dimmed" lineClamp={1}>
225
- {item.error ?? "\u2014"}
226
- </Text>
227
- ),
228
- },
229
- key: {
230
- label: "Key",
231
- defaultHidden: true,
232
- value: (item) => (
233
- <Text size="xs" c="dimmed" ff="monospace">
234
- {item.key ?? "\u2014"}
235
- </Text>
236
- ),
237
- },
238
- workerId: {
239
- label: "Worker",
240
- defaultHidden: true,
241
- value: (item) => (
242
- <Text size="xs" c="dimmed" ff="monospace">
243
- {item.workerId ?? "\u2014"}
244
- </Text>
245
- ),
246
- },
247
- }}
248
- rowActions={(item) => [
249
- {
250
- label: "Retry",
251
- icon: IconRefresh,
252
- onClick: () => handleRetry(item.id),
253
- visible: item.can?.retry,
254
- },
255
- {
256
- label: "Cancel",
257
- icon: IconCircleX,
258
- onClick: () => handleCancel(item.id),
259
- visible: item.can?.cancel,
260
- },
261
- ]}
262
- panel={{
263
- can: (item) => Boolean(item.error || item.key || item.workerId),
264
- render: (item) => (
265
- <Flex direction="column" gap="sm" p="sm">
266
- {item.error && (
267
- <Flex direction="column" gap={2}>
268
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
269
- Error
270
- </Text>
271
- <Paper p="xs" radius="sm" withBorder>
272
- <Text
273
- size="xs"
274
- style={{
275
- whiteSpace: "pre-wrap",
276
- wordBreak: "break-word",
277
- }}
278
- >
279
- {item.error}
280
- </Text>
281
- </Paper>
282
- </Flex>
283
- )}
284
- <Flex gap="lg" wrap="wrap">
285
- <Flex direction="column" gap={2}>
286
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
287
- ID
288
- </Text>
289
- <Text size="xs" ff="monospace">
290
- {item.id}
291
- </Text>
292
- </Flex>
293
- {item.key && (
294
- <Flex direction="column" gap={2}>
295
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
296
- Key
297
- </Text>
298
- <Text size="xs" ff="monospace">
299
- {item.key}
300
- </Text>
301
- </Flex>
302
- )}
303
- {item.workerId && (
304
- <Flex direction="column" gap={2}>
305
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
306
- Worker
307
- </Text>
308
- <Text size="xs" ff="monospace">
309
- {item.workerId}
310
- </Text>
311
- </Flex>
312
- )}
313
- {item.triggeredByName && (
314
- <Flex direction="column" gap={2}>
315
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
316
- Triggered By
317
- </Text>
318
- <Text size="xs">{item.triggeredByName}</Text>
319
- </Flex>
320
- )}
321
- </Flex>
322
- </Flex>
323
- ),
324
- }}
325
- drawer={(item) => (
326
- <ExecutionDetailContent
327
- item={item}
328
- onRetry={handleRetry}
329
- onCancel={handleCancel}
330
- />
331
- )}
332
- />
333
- </Flex>
334
- );
335
- };
64
+ const emptyFilters = t.object({});
336
65
 
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+ // ExecutionDetailContent
337
68
  // ─────────────────────────────────────────────────────────────────────────────
338
69
 
339
70
  const ExecutionDetailContent = ({
@@ -535,49 +266,35 @@ const ExecutionDetailContent = ({
535
266
  </Flex>
536
267
 
537
268
  {/* Details */}
538
- <Paper p="sm" radius="md" withBorder>
539
- <Text size="sm" fw={600} mb="xs">
540
- Details
541
- </Text>
269
+ <Section title="Details" p="sm">
542
270
  <DetailList items={detailItems} columns={2} />
543
- </Paper>
271
+ </Section>
544
272
 
545
273
  {/* Payload */}
546
274
  {detail.payload && (
547
- <Paper p="sm" radius="md" withBorder>
548
- <Text size="sm" fw={600} mb="xs">
549
- Payload
550
- </Text>
275
+ <Section title="Payload" p="sm">
551
276
  <Code block>{JSON.stringify(detail.payload, null, 2)}</Code>
552
- </Paper>
277
+ </Section>
553
278
  )}
554
279
 
555
280
  {/* Error */}
556
281
  {detail.error && (
557
- <Paper p="sm" radius="md" withBorder>
558
- <Text size="sm" fw={600} mb="xs">
559
- Error
282
+ <Section title="Error" p="sm">
283
+ <Text
284
+ size="sm"
285
+ style={{
286
+ whiteSpace: "pre-wrap",
287
+ wordBreak: "break-word",
288
+ }}
289
+ >
290
+ {detail.error}
560
291
  </Text>
561
- <Paper p="xs" radius="sm" withBorder>
562
- <Text
563
- size="sm"
564
- style={{
565
- whiteSpace: "pre-wrap",
566
- wordBreak: "break-word",
567
- }}
568
- >
569
- {detail.error}
570
- </Text>
571
- </Paper>
572
- </Paper>
292
+ </Section>
573
293
  )}
574
294
 
575
295
  {/* Logs */}
576
296
  {detail.logs && detail.logs.length > 0 && (
577
- <Paper p="sm" radius="md" withBorder>
578
- <Text size="sm" fw={600} mb="xs">
579
- Logs ({detail.logs.length})
580
- </Text>
297
+ <Section title={`Logs (${detail.logs.length})`} p="sm">
581
298
  <Flex
582
299
  direction="column"
583
300
  style={{ maxHeight: 400, overflowY: "auto" }}
@@ -620,10 +337,426 @@ const ExecutionDetailContent = ({
620
337
  </Table.Tbody>
621
338
  </Table>
622
339
  </Flex>
623
- </Paper>
340
+ </Section>
341
+ )}
342
+ </Flex>
343
+ );
344
+ };
345
+
346
+ // ─────────────────────────────────────────────────────────────────────────────
347
+ // JobExecutionsPanel
348
+ // ─────────────────────────────────────────────────────────────────────────────
349
+
350
+ const JobExecutionsPanel = ({
351
+ item,
352
+ cronMap,
353
+ failureMap,
354
+ }: {
355
+ item: JobRegistration;
356
+ cronMap: Map<string, JobCronInfo>;
357
+ failureMap: Map<string, JobFailure>;
358
+ }) => {
359
+ const client = useClient<AdminJobController>();
360
+ const { l } = useI18n();
361
+ const toast = useToast();
362
+ const dialog = useDialog();
363
+ const [refreshKey, setRefreshKey] = useState(0);
364
+
365
+ const cron = cronMap.get(item.name);
366
+ const failure = failureMap.get(item.name);
367
+
368
+ const handleRetry = useCallback(
369
+ async (id: string) => {
370
+ try {
371
+ await client.retryJobExecution({ params: { id } });
372
+ toast.success("Execution retried");
373
+ setRefreshKey((k) => k + 1);
374
+ } catch {
375
+ toast.danger("Failed to retry execution");
376
+ }
377
+ },
378
+ [client, toast],
379
+ );
380
+
381
+ const handleCancel = useCallback(
382
+ async (id: string) => {
383
+ const confirmed = await dialog.confirm({
384
+ title: "Cancel Execution",
385
+ message: "Are you sure you want to cancel this execution?",
386
+ confirmLabel: "Cancel",
387
+ confirmColor: "red",
388
+ });
389
+
390
+ if (!confirmed) return;
391
+
392
+ try {
393
+ await client.cancelJobExecution({ params: { id } });
394
+ toast.success("Execution cancelled");
395
+ setRefreshKey((k) => k + 1);
396
+ } catch {
397
+ toast.danger("Failed to cancel execution");
398
+ }
399
+ },
400
+ [client, dialog, toast],
401
+ );
402
+
403
+ const detailItems: DetailListItem[] = [
404
+ {
405
+ label: "Cron",
406
+ value: item.cron ? (
407
+ <Text size="sm" ff="monospace">
408
+ {item.cron}
409
+ </Text>
410
+ ) : undefined,
411
+ hidden: !item.cron,
412
+ },
413
+ {
414
+ label: "Timeout",
415
+ value: item.timeout,
416
+ hidden: !item.timeout,
417
+ },
418
+ {
419
+ label: "Retry",
420
+ value: item.retry
421
+ ? `${item.retry.retries}x${item.retry.hasBackoff ? " (backoff)" : ""}`
422
+ : undefined,
423
+ hidden: !item.retry,
424
+ },
425
+ {
426
+ label: "Batch",
427
+ value: item.batch
428
+ ? `${item.batch.size} / ${item.batch.window}`
429
+ : undefined,
430
+ hidden: !item.batch,
431
+ },
432
+ {
433
+ label: "Schema",
434
+ value: item.hasSchema ? "Yes" : "No",
435
+ },
436
+ ];
437
+
438
+ return (
439
+ <Flex direction="column" gap="sm" p="sm">
440
+ {/* Last cron execution */}
441
+ {cron?.lastExecution && (
442
+ <Flex gap="lg" wrap="wrap" align="center">
443
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
444
+ Last Run
445
+ </Text>
446
+ <Flex align="center" gap={4}>
447
+ {cron.lastExecution.status === "completed" ? (
448
+ <IconCircleCheck size={14} color="var(--mantine-color-dimmed)" />
449
+ ) : (
450
+ <IconCircleX size={14} color="var(--mantine-color-dimmed)" />
451
+ )}
452
+ <Text size="xs" tt="capitalize">
453
+ {cron.lastExecution.status}
454
+ </Text>
455
+ </Flex>
456
+ {cron.lastExecution.startedAt && (
457
+ <Text size="xs" c="dimmed">
458
+ {l(cron.lastExecution.startedAt, { date: "fromNow" })}
459
+ </Text>
460
+ )}
461
+ {cron.lastExecution.error && (
462
+ <Text size="xs" c="dimmed" lineClamp={1}>
463
+ {cron.lastExecution.error}
464
+ </Text>
465
+ )}
466
+ </Flex>
467
+ )}
468
+
469
+ {/* Failures */}
470
+ {failure && (
471
+ <Flex gap="lg" wrap="wrap" align="center">
472
+ <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
473
+ Failures (7d)
474
+ </Text>
475
+ <Text size="xs" fw={500}>
476
+ {failure.failures}
477
+ </Text>
478
+ {failure.lastError && (
479
+ <Text size="xs" c="dimmed" lineClamp={1} style={{ maxWidth: 400 }}>
480
+ {failure.lastError}
481
+ </Text>
482
+ )}
483
+ </Flex>
624
484
  )}
485
+
486
+ {/* Job config */}
487
+ <DetailList items={detailItems} columns={3} />
488
+
489
+ {/* Executions table */}
490
+ <DataTable<JobExecutionResource, typeof emptyFilters>
491
+ key={`executions-${item.name}-${refreshKey}`}
492
+ submitOnInit
493
+ defaultSize={10}
494
+ tableProps={{
495
+ horizontalSpacing: "sm",
496
+ verticalSpacing: "sm",
497
+ }}
498
+ items={async () => {
499
+ const response = await client.findJobExecutions({
500
+ query: { job: item.name },
501
+ });
502
+ return response as Page<JobExecutionResource>;
503
+ }}
504
+ columns={{
505
+ status: {
506
+ label: "Status",
507
+ value: (exec) => {
508
+ const color =
509
+ exec.status === "completed"
510
+ ? "green"
511
+ : exec.status === "running"
512
+ ? "blue"
513
+ : exec.status === "failed" || exec.status === "dead"
514
+ ? "red"
515
+ : exec.status === "cancelled"
516
+ ? "yellow"
517
+ : "gray";
518
+ return (
519
+ <Badge size="sm" variant="light" color={color}>
520
+ {exec.status}
521
+ </Badge>
522
+ );
523
+ },
524
+ },
525
+ jobName: {
526
+ label: "Job",
527
+ value: (exec) => (
528
+ <Text size="sm" fw={500} ff="monospace">
529
+ {exec.jobName}
530
+ </Text>
531
+ ),
532
+ },
533
+ attempt: {
534
+ label: "Attempt",
535
+ value: (exec) => (
536
+ <Text size="sm" ff="monospace">
537
+ {exec.attempt}/{exec.maxAttempts}
538
+ </Text>
539
+ ),
540
+ },
541
+ startedAt: {
542
+ label: "Started",
543
+ value: (exec) => (
544
+ <Text size="xs" c="dimmed">
545
+ {exec.startedAt
546
+ ? l(exec.startedAt, { date: "fromNow" })
547
+ : "\u2014"}
548
+ </Text>
549
+ ),
550
+ },
551
+ duration: {
552
+ label: "Duration",
553
+ value: (exec) => (
554
+ <Text size="xs" c="dimmed" ff="monospace">
555
+ {exec.startedAt &&
556
+ (exec.completedAt || exec.status === "running")
557
+ ? formatDuration(exec.startedAt, exec.completedAt)
558
+ : "\u2014"}
559
+ </Text>
560
+ ),
561
+ },
562
+ }}
563
+ rowActions={(exec) => [
564
+ {
565
+ label: "Retry",
566
+ icon: IconRefresh,
567
+ onClick: () => handleRetry(exec.id),
568
+ visible: exec.can?.retry,
569
+ },
570
+ {
571
+ label: "Cancel",
572
+ icon: IconCircleX,
573
+ onClick: () => handleCancel(exec.id),
574
+ visible: exec.can?.cancel,
575
+ },
576
+ ]}
577
+ drawer={(exec) => (
578
+ <ExecutionDetailContent
579
+ item={exec}
580
+ onRetry={handleRetry}
581
+ onCancel={handleCancel}
582
+ />
583
+ )}
584
+ />
585
+ </Flex>
586
+ );
587
+ };
588
+
589
+ // ─────────────────────────────────────────────────────────────────────────────
590
+ // AdminJobs (main page)
591
+ // ─────────────────────────────────────────────────────────────────────────────
592
+
593
+ const AdminJobs = () => {
594
+ const client = useClient<AdminJobController>();
595
+ const toast = useToast();
596
+ const dialog = useDialog();
597
+ const [refreshKey, setRefreshKey] = useState(0);
598
+
599
+ // Extra data for enriched panels
600
+ const [cronMap, setCronMap] = useState<Map<string, JobCronInfo>>(new Map());
601
+ const [queueMap, setQueueMap] = useState<Map<string, JobQueueDepth>>(
602
+ new Map(),
603
+ );
604
+ const [failureMap, setFailureMap] = useState<Map<string, JobFailure>>(
605
+ new Map(),
606
+ );
607
+
608
+ const loadExtraData = useCallback(async () => {
609
+ try {
610
+ const [cronData, queueData, failureData] = await Promise.all([
611
+ client.getCronJobs(),
612
+ client.getJobQueueDepth(),
613
+ client.getJobTopFailures(),
614
+ ]);
615
+ setCronMap(new Map(cronData.map((c) => [c.name, c])));
616
+ setQueueMap(new Map(queueData.map((q) => [q.jobName, q])));
617
+ setFailureMap(new Map(failureData.map((f) => [f.jobName, f])));
618
+ } catch {
619
+ // non-critical
620
+ }
621
+ }, [client]);
622
+
623
+ useEffect(() => {
624
+ loadExtraData();
625
+ }, [loadExtraData, refreshKey]);
626
+
627
+ const handleTriggerJob = useCallback(
628
+ async (name: string) => {
629
+ const confirmed = await dialog.confirm({
630
+ title: "Trigger Job",
631
+ message: `Are you sure you want to trigger "${name}" manually?`,
632
+ confirmLabel: "Trigger",
633
+ confirmColor: "blue",
634
+ });
635
+
636
+ if (!confirmed) return;
637
+
638
+ return client.triggerJob({ body: { name } }).then(() => {
639
+ toast.success(`Job "${name}" triggered`);
640
+ setRefreshKey((k) => k + 1);
641
+ });
642
+ },
643
+ [client, dialog, toast],
644
+ );
645
+
646
+ return (
647
+ <Flex p="md" flex={1} direction="column" gap="md">
648
+ <DataTable<JobRegistration, typeof registryFilters>
649
+ key={`registry-${refreshKey}`}
650
+ submitOnInit
651
+ typeFormProps={{
652
+ skipSubmitButton: true,
653
+ columns: 1,
654
+ }}
655
+ tableProps={{
656
+ horizontalSpacing: "sm",
657
+ verticalSpacing: "sm",
658
+ }}
659
+ onFilterChange={(_key, _value, form) => form.submit()}
660
+ filters={registryFilters}
661
+ items={async (filters) => {
662
+ const items = await client.getJobRegistry();
663
+ const filtered = filters.type
664
+ ? items.filter((i) => i.type === filters.type)
665
+ : items;
666
+ return { content: filtered };
667
+ }}
668
+ columns={{
669
+ name: {
670
+ label: "Name",
671
+ value: (item) => (
672
+ <Text size="sm" fw={500} ff="monospace">
673
+ {item.name}
674
+ </Text>
675
+ ),
676
+ },
677
+ type: {
678
+ label: "Type",
679
+ value: (item) => (
680
+ <Badge size="sm" variant="default">
681
+ {item.type}
682
+ </Badge>
683
+ ),
684
+ },
685
+ priority: {
686
+ label: "Priority",
687
+ value: (item) => (
688
+ <Text size="sm" tt="capitalize">
689
+ {item.priority}
690
+ </Text>
691
+ ),
692
+ },
693
+ concurrency: {
694
+ label: "Concurrency",
695
+ value: (item) => (
696
+ <Text size="sm" ff="monospace">
697
+ {item.concurrency}
698
+ </Text>
699
+ ),
700
+ },
701
+ queue: {
702
+ label: "Queue",
703
+ value: (item) => {
704
+ const q = queueMap.get(item.name);
705
+ if (
706
+ !q ||
707
+ q.pending + q.running + q.scheduled + q.retrying + q.dead === 0
708
+ ) {
709
+ return (
710
+ <Text size="xs" c="dimmed">
711
+
712
+ </Text>
713
+ );
714
+ }
715
+ return (
716
+ <Flex gap={4}>
717
+ {q.running > 0 && (
718
+ <Badge size="xs" variant="default">
719
+ {q.running} run
720
+ </Badge>
721
+ )}
722
+ {q.pending > 0 && (
723
+ <Badge size="xs" variant="default">
724
+ {q.pending} pen
725
+ </Badge>
726
+ )}
727
+ {q.retrying > 0 && (
728
+ <Badge size="xs" variant="default">
729
+ {q.retrying} retry
730
+ </Badge>
731
+ )}
732
+ {q.dead > 0 && (
733
+ <Badge size="xs" variant="default">
734
+ {q.dead} dead
735
+ </Badge>
736
+ )}
737
+ </Flex>
738
+ );
739
+ },
740
+ },
741
+ }}
742
+ rowActions={(item) => [
743
+ {
744
+ label: "Trigger",
745
+ color: "blue",
746
+ icon: IconPlayerPlay,
747
+ onClick: () => handleTriggerJob(item.name),
748
+ },
749
+ ]}
750
+ panel={(item) => (
751
+ <JobExecutionsPanel
752
+ item={item}
753
+ cronMap={cronMap}
754
+ failureMap={failureMap}
755
+ />
756
+ )}
757
+ />
625
758
  </Flex>
626
759
  );
627
760
  };
628
761
 
629
- export default AdminJobExecutions;
762
+ export default AdminJobs;