@alepha/ui 0.19.0 → 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 (151) hide show
  1. package/dist/admin/{AdminApiKeys-Bt1PjO6o.js → AdminApiKeys-C2ze85eD.js} +2 -2
  2. package/dist/admin/{AdminApiKeys-Bt1PjO6o.js.map → AdminApiKeys-C2ze85eD.js.map} +1 -1
  3. package/dist/admin/{AdminAudits-C7c1CN4c.js → AdminAudits-BIj81e4k.js} +2 -2
  4. package/dist/admin/{AdminAudits-C7c1CN4c.js.map → AdminAudits-BIj81e4k.js.map} +1 -1
  5. package/dist/admin/{AdminDashboard-C3RXpTp6.js → AdminDashboard-PMVzrwSu.js} +2 -2
  6. package/dist/admin/{AdminDashboard-C3RXpTp6.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-D-G8RIlr.js → AdminJobs-D1_QGCDy.js} +400 -356
  10. package/dist/admin/AdminJobs-D1_QGCDy.js.map +1 -0
  11. package/dist/admin/{AdminLayout-BmZ9mtXh.js → AdminLayout-BNiwiw2D.js} +2 -2
  12. package/dist/admin/{AdminLayout-BmZ9mtXh.js.map → AdminLayout-BNiwiw2D.js.map} +1 -1
  13. package/dist/admin/{AdminNotifications-DHdzksww.js → AdminNotifications-DSKQtUfn.js} +84 -122
  14. package/dist/admin/AdminNotifications-DSKQtUfn.js.map +1 -0
  15. package/dist/admin/{AdminParameters-CyZQSXnN.js → AdminParameters-CoB7EhyM.js} +2 -2
  16. package/dist/admin/{AdminParameters-CyZQSXnN.js.map → AdminParameters-CoB7EhyM.js.map} +1 -1
  17. package/dist/admin/{AdminSessions--xwELDSO.js → AdminSessions-DFbFcrJQ.js} +2 -2
  18. package/dist/admin/{AdminSessions--xwELDSO.js.map → AdminSessions-DFbFcrJQ.js.map} +1 -1
  19. package/dist/admin/{AdminUserLayout-DvBTG5gd.js → AdminUserLayout-fSfi3KMm.js} +3 -3
  20. package/dist/admin/{AdminUserLayout-DvBTG5gd.js.map → AdminUserLayout-fSfi3KMm.js.map} +1 -1
  21. package/dist/admin/{AdminUserProfile-CzsPBl6Z.js → AdminUserProfile-_C-h8vUK.js} +3 -3
  22. package/dist/admin/{AdminUserProfile-CzsPBl6Z.js.map → AdminUserProfile-_C-h8vUK.js.map} +1 -1
  23. package/dist/admin/{AdminUserSessions-C-aUnhVN.js → AdminUserSessions-KpJHIeQo.js} +2 -2
  24. package/dist/admin/{AdminUserSessions-C-aUnhVN.js.map → AdminUserSessions-KpJHIeQo.js.map} +1 -1
  25. package/dist/admin/{AdminUsers-BYwei5sj.js → AdminUsers-DcVrzdQP.js} +2 -2
  26. package/dist/admin/{AdminUsers-BYwei5sj.js.map → AdminUsers-DcVrzdQP.js.map} +1 -1
  27. package/dist/admin/{AuthLayout-CkPGLJku.js → AuthLayout-CazfLzcf.js} +2 -2
  28. package/dist/admin/{AuthLayout-CkPGLJku.js.map → AuthLayout-CazfLzcf.js.map} +1 -1
  29. package/dist/admin/{Login-DSBqNsZc.js → Login-CaMjUrDP.js} +2 -2
  30. package/dist/admin/{Login-DSBqNsZc.js.map → Login-CaMjUrDP.js.map} +1 -1
  31. package/dist/admin/{Profile-CDRjJo0P.js → Profile-Ca4fZX15.js} +2 -2
  32. package/dist/{auth/Profile-Cy93pNTw.js.map → admin/Profile-Ca4fZX15.js.map} +1 -1
  33. package/dist/admin/{Register-4QGFOnfh.js → Register-C5DyKWPO.js} +2 -2
  34. package/dist/{demo/Register-KKZwr_lL.js.map → admin/Register-C5DyKWPO.js.map} +1 -1
  35. package/dist/admin/{ResetPassword-Gxc9L_mY.js → ResetPassword-BA5sAgXo.js} +2 -2
  36. package/dist/{auth/ResetPassword-B61QPlQi.js.map → admin/ResetPassword-BA5sAgXo.js.map} +1 -1
  37. package/dist/admin/{VerifyEmail-D7G5NnaN.js → VerifyEmail-DKNXROj_.js} +2 -2
  38. package/dist/{auth/VerifyEmail-CqBJ11id.js.map → admin/VerifyEmail-DKNXROj_.js.map} +1 -1
  39. package/dist/admin/{adminUserAtom-DCi4wf-v.js → adminUserAtom-BLNc7XbT.js} +1 -1
  40. package/dist/admin/{adminUserAtom-DCi4wf-v.js.map → adminUserAtom-BLNc7XbT.js.map} +1 -1
  41. package/dist/admin/{core-D1AbU50V.js → core-CJCEx18C.js} +111 -4
  42. package/dist/admin/core-CJCEx18C.js.map +1 -0
  43. package/dist/admin/index.d.ts +21 -3
  44. package/dist/admin/index.d.ts.map +1 -1
  45. package/dist/admin/index.js +29 -58
  46. package/dist/admin/index.js.map +1 -1
  47. package/dist/auth/{AuthLayout-CfRKcTqP.js → AuthLayout-vXPcCVzp.js} +2 -2
  48. package/dist/auth/{AuthLayout-CfRKcTqP.js.map → AuthLayout-vXPcCVzp.js.map} +1 -1
  49. package/dist/auth/{Login-DJyweoPS.js → Login-Dg08QR20.js} +2 -2
  50. package/dist/{demo/Login-CqG1iJbn.js.map → auth/Login-Dg08QR20.js.map} +1 -1
  51. package/dist/{demo/Profile-C0ojJCaG.js → auth/Profile-Bb5O1yeh.js} +2 -2
  52. package/dist/{admin/Profile-CDRjJo0P.js.map → auth/Profile-Bb5O1yeh.js.map} +1 -1
  53. package/dist/auth/{Register-CSqzzitW.js → Register-B2AN71NC.js} +2 -2
  54. package/dist/auth/{Register-CSqzzitW.js.map → Register-B2AN71NC.js.map} +1 -1
  55. package/dist/{demo/ResetPassword-DMrLFEtr.js → auth/ResetPassword-BLxwzbDj.js} +2 -2
  56. package/dist/{admin/ResetPassword-Gxc9L_mY.js.map → auth/ResetPassword-BLxwzbDj.js.map} +1 -1
  57. package/dist/auth/{VerifyEmail-CqBJ11id.js → VerifyEmail-CSDOk3Zm.js} +2 -2
  58. package/dist/{demo/VerifyEmail-BFCAFz6T.js.map → auth/VerifyEmail-CSDOk3Zm.js.map} +1 -1
  59. package/dist/auth/{core-C6D3pazL.js → core-DuGkjPiU.js} +2 -1
  60. package/dist/auth/core-DuGkjPiU.js.map +1 -0
  61. package/dist/auth/index.d.ts +20 -0
  62. package/dist/auth/index.d.ts.map +1 -1
  63. package/dist/auth/index.js +11 -11
  64. package/dist/core/index.d.ts +69 -17
  65. package/dist/core/index.d.ts.map +1 -1
  66. package/dist/core/index.js +110 -8
  67. package/dist/core/index.js.map +1 -1
  68. package/dist/demo/{AuthLayout-Dq5tSLSc.js → AuthLayout-DPsOOG4u.js} +2 -2
  69. package/dist/demo/{AuthLayout-Dq5tSLSc.js.map → AuthLayout-DPsOOG4u.js.map} +1 -1
  70. package/dist/demo/{DemoButton-_Ws2w-J0.js → DemoButton-wzcqGk4u.js} +3 -3
  71. package/dist/demo/{DemoButton-_Ws2w-J0.js.map → DemoButton-wzcqGk4u.js.map} +1 -1
  72. package/dist/demo/{DemoControlSelect-ChP4ZOpQ.js → DemoControlSelect-CMWvQ6Gm.js} +3 -3
  73. package/dist/demo/{DemoControlSelect-ChP4ZOpQ.js.map → DemoControlSelect-CMWvQ6Gm.js.map} +1 -1
  74. package/dist/demo/{DemoDataTable-Hwf_UUni.js → DemoDataTable-CHsAP3e2.js} +3 -3
  75. package/dist/demo/{DemoDataTable-Hwf_UUni.js.map → DemoDataTable-CHsAP3e2.js.map} +1 -1
  76. package/dist/demo/{DemoDialog-B01OMVRd.js → DemoDialog-Co2IePxX.js} +2 -2
  77. package/dist/demo/{DemoDialog-B01OMVRd.js.map → DemoDialog-Co2IePxX.js.map} +1 -1
  78. package/dist/demo/{DemoFlex-870PEl0V.js → DemoFlex-OEwQt5do.js} +3 -3
  79. package/dist/demo/{DemoFlex-870PEl0V.js.map → DemoFlex-OEwQt5do.js.map} +1 -1
  80. package/dist/demo/DemoHeading-Db-XkQIK.js +69 -0
  81. package/dist/demo/DemoHeading-Db-XkQIK.js.map +1 -0
  82. package/dist/demo/{DemoHome-DRbL2eGf.js → DemoHome-Cyp29ygy.js} +2 -2
  83. package/dist/demo/{DemoHome-DRbL2eGf.js.map → DemoHome-Cyp29ygy.js.map} +1 -1
  84. package/dist/demo/{DemoJsonViewer-DoABiqBW.js → DemoJsonViewer-DXtCeMzH.js} +3 -3
  85. package/dist/demo/{DemoJsonViewer-DoABiqBW.js.map → DemoJsonViewer-DXtCeMzH.js.map} +1 -1
  86. package/dist/demo/{DemoLayout-CN_PDCX2.js → DemoLayout-hh9VmZQP.js} +2 -2
  87. package/dist/demo/{DemoLayout-CN_PDCX2.js.map → DemoLayout-hh9VmZQP.js.map} +1 -1
  88. package/dist/demo/{DemoLogin-B5x-ug3Q.js → DemoLogin-DX7mnmkh.js} +13 -8
  89. package/dist/demo/{DemoLogin-B5x-ug3Q.js.map → DemoLogin-DX7mnmkh.js.map} +1 -1
  90. package/dist/demo/{DemoRegister-Q6sg2xuV.js → DemoRegister-DVcZl04m.js} +13 -8
  91. package/dist/demo/{DemoRegister-Q6sg2xuV.js.map → DemoRegister-DVcZl04m.js.map} +1 -1
  92. package/dist/demo/{DemoResetPassword-DrqZfmEw.js → DemoResetPassword-CPENlZH5.js} +13 -8
  93. package/dist/demo/{DemoResetPassword-DrqZfmEw.js.map → DemoResetPassword-CPENlZH5.js.map} +1 -1
  94. package/dist/demo/{DemoSidebar-CfKS6w1o.js → DemoSidebar-CGu7DZeM.js} +3 -3
  95. package/dist/demo/{DemoSidebar-CfKS6w1o.js.map → DemoSidebar-CGu7DZeM.js.map} +1 -1
  96. package/dist/demo/{DemoText-pT6Gi5b5.js → DemoText-DYUJ7bY_.js} +3 -3
  97. package/dist/demo/{DemoText-pT6Gi5b5.js.map → DemoText-DYUJ7bY_.js.map} +1 -1
  98. package/dist/demo/{DemoToast-I13NBzQQ.js → DemoToast-CgdnZNvx.js} +2 -2
  99. package/dist/demo/{DemoToast-I13NBzQQ.js.map → DemoToast-CgdnZNvx.js.map} +1 -1
  100. package/dist/demo/{DemoTypeForm-BqzcrtvN.js → DemoTypeForm-Pims-cGa.js} +3 -3
  101. package/dist/demo/{DemoTypeForm-BqzcrtvN.js.map → DemoTypeForm-Pims-cGa.js.map} +1 -1
  102. package/dist/demo/{DemoVerifyEmail-HwD8xfQw.js → DemoVerifyEmail-C7B3xxch.js} +8 -8
  103. package/dist/demo/{DemoVerifyEmail-HwD8xfQw.js.map → DemoVerifyEmail-C7B3xxch.js.map} +1 -1
  104. package/dist/demo/{Login-CqG1iJbn.js → Login-pwMF4TUj.js} +2 -2
  105. package/dist/{auth/Login-DJyweoPS.js.map → demo/Login-pwMF4TUj.js.map} +1 -1
  106. package/dist/{auth/Profile-Cy93pNTw.js → demo/Profile-BliZapZS.js} +2 -2
  107. package/dist/demo/{Profile-C0ojJCaG.js.map → Profile-BliZapZS.js.map} +1 -1
  108. package/dist/demo/{Register-KKZwr_lL.js → Register-CiwAT7Hy.js} +2 -2
  109. package/dist/{admin/Register-4QGFOnfh.js.map → demo/Register-CiwAT7Hy.js.map} +1 -1
  110. package/dist/{auth/ResetPassword-B61QPlQi.js → demo/ResetPassword-l9Vg4JE-.js} +2 -2
  111. package/dist/demo/{ResetPassword-DMrLFEtr.js.map → ResetPassword-l9Vg4JE-.js.map} +1 -1
  112. package/dist/demo/{Showcase-D49Wud2v.js → Showcase-CX6bDgwe.js} +2 -2
  113. package/dist/demo/{Showcase-D49Wud2v.js.map → Showcase-CX6bDgwe.js.map} +1 -1
  114. package/dist/demo/{VerifyEmail-BFCAFz6T.js → VerifyEmail-CAB-OS7i.js} +2 -2
  115. package/dist/{admin/VerifyEmail-D7G5NnaN.js.map → demo/VerifyEmail-CAB-OS7i.js.map} +1 -1
  116. package/dist/demo/{auth-D9qTZzCa.js → auth-uegJAdKu.js} +8 -8
  117. package/dist/demo/{auth-D9qTZzCa.js.map → auth-uegJAdKu.js.map} +1 -1
  118. package/dist/demo/{core-DRtQklr3.js → core-B4LVHzPn.js} +111 -9
  119. package/dist/demo/core-B4LVHzPn.js.map +1 -0
  120. package/dist/demo/index.js +19 -19
  121. package/dist/demo/index.js.map +1 -1
  122. package/package.json +6 -9
  123. package/src/admin/AdminRouter.tsx +5 -37
  124. package/src/admin/components/files/AdminFiles.tsx +123 -1
  125. package/src/admin/components/jobs/{AdminJobExecutions.tsx → AdminJobs.tsx} +450 -317
  126. package/src/admin/components/notifications/AdminNotifications.tsx +11 -25
  127. package/src/core/components/Section.tsx +109 -0
  128. package/src/core/components/SectionHeader.tsx +106 -0
  129. package/src/core/index.ts +4 -1
  130. package/src/core/table/components/DataTable.tsx +5 -1
  131. package/src/demo/DemoRouter.ts +1 -1
  132. package/src/demo/components/auth/DemoLogin.tsx +5 -0
  133. package/src/demo/components/auth/DemoRegister.tsx +5 -0
  134. package/src/demo/components/auth/DemoResetPassword.tsx +5 -0
  135. package/src/demo/components/core/DemoHeading.tsx +56 -3
  136. package/dist/admin/AdminFiles-31ivR6Wq.js +0 -110
  137. package/dist/admin/AdminFiles-31ivR6Wq.js.map +0 -1
  138. package/dist/admin/AdminJobDashboard-BABLe7hL.js +0 -402
  139. package/dist/admin/AdminJobDashboard-BABLe7hL.js.map +0 -1
  140. package/dist/admin/AdminJobExecutions-D-G8RIlr.js.map +0 -1
  141. package/dist/admin/AdminJobRegistry-oIS3K9NX.js +0 -269
  142. package/dist/admin/AdminJobRegistry-oIS3K9NX.js.map +0 -1
  143. package/dist/admin/AdminNotifications-DHdzksww.js.map +0 -1
  144. package/dist/admin/core-D1AbU50V.js.map +0 -1
  145. package/dist/auth/core-C6D3pazL.js.map +0 -1
  146. package/dist/demo/DemoHeading-C1YR27fz.js +0 -17
  147. package/dist/demo/DemoHeading-C1YR27fz.js.map +0 -1
  148. package/dist/demo/core-DRtQklr3.js.map +0 -1
  149. package/src/admin/components/jobs/AdminJobDashboard.tsx +0 -380
  150. package/src/admin/components/jobs/AdminJobRegistry.tsx +0 -301
  151. 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;