@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
@@ -1,772 +0,0 @@
1
- import {
2
- ActionButton,
3
- DataTable,
4
- Flex,
5
- Text,
6
- useDialog,
7
- useToast,
8
- } from "@alepha/ui";
9
- import {
10
- ActionIcon,
11
- Badge,
12
- Box,
13
- Card,
14
- Group,
15
- Paper,
16
- Progress,
17
- RingProgress,
18
- ScrollArea,
19
- SimpleGrid,
20
- Skeleton,
21
- Stack,
22
- Tabs,
23
- ThemeIcon,
24
- Tooltip,
25
- useMantineTheme,
26
- } from "@mantine/core";
27
- import {
28
- IconAlertTriangle,
29
- IconCircleCheck,
30
- IconCircleX,
31
- IconClock,
32
- IconPlayerPlay,
33
- IconRefresh,
34
- IconTerminal2,
35
- } from "@tabler/icons-react";
36
- import { type Page, t } from "alepha";
37
- import {
38
- type AdminJobController,
39
- type JobExecutionEntity,
40
- jobExecutions,
41
- } from "alepha/api/jobs";
42
- import { useClient } from "alepha/react";
43
- import { useI18n } from "alepha/react/i18n";
44
- import { useCallback, useEffect, useState } from "react";
45
-
46
- // ─────────────────────────────────────────────────────────────────────────────
47
- // Types
48
- // ─────────────────────────────────────────────────────────────────────────────
49
-
50
- interface JobStats {
51
- name: string;
52
- total: number;
53
- completed: number;
54
- failed: number;
55
- running: number;
56
- avgDuration: number;
57
- lastRun?: Date;
58
- lastStatus?: "COMPLETED" | "FAILED" | "STARTED";
59
- }
60
-
61
- interface LogEntry {
62
- level: string;
63
- message: string;
64
- service: string;
65
- module: string;
66
- timestamp: number;
67
- data?: unknown;
68
- }
69
-
70
- // ─────────────────────────────────────────────────────────────────────────────
71
- // Utilities
72
- // ─────────────────────────────────────────────────────────────────────────────
73
-
74
- const formatDuration = (
75
- start: Date | string,
76
- end?: Date | string | null,
77
- ): string => {
78
- const startTime = new Date(start).getTime();
79
- const endTime = end ? new Date(end).getTime() : Date.now();
80
- const duration = endTime - startTime;
81
-
82
- if (duration < 1000) return `${duration}ms`;
83
- if (duration < 60000) return `${(duration / 1000).toFixed(1)}s`;
84
- if (duration < 3600000)
85
- return `${Math.floor(duration / 60000)}m ${Math.floor((duration % 60000) / 1000)}s`;
86
- return `${Math.floor(duration / 3600000)}h ${Math.floor((duration % 3600000) / 60000)}m`;
87
- };
88
-
89
- const getStatusColor = (status: string) => {
90
- switch (status) {
91
- case "COMPLETED":
92
- return "teal";
93
- case "FAILED":
94
- return "red";
95
- case "STARTED":
96
- return "blue";
97
- default:
98
- return "gray";
99
- }
100
- };
101
-
102
- const getStatusIcon = (status: string, size = 14) => {
103
- switch (status) {
104
- case "COMPLETED":
105
- return <IconCircleCheck size={size} />;
106
- case "FAILED":
107
- return <IconCircleX size={size} />;
108
- case "STARTED":
109
- return <IconPlayerPlay size={size} />;
110
- default:
111
- return <IconClock size={size} />;
112
- }
113
- };
114
-
115
- const getLogLevelColor = (level: string) => {
116
- switch (level) {
117
- case "ERROR":
118
- return "red";
119
- case "WARN":
120
- return "yellow";
121
- case "INFO":
122
- return "blue";
123
- case "DEBUG":
124
- return "gray";
125
- case "TRACE":
126
- return "dimmed";
127
- default:
128
- return "dimmed";
129
- }
130
- };
131
-
132
- // ─────────────────────────────────────────────────────────────────────────────
133
- // Job Card Component
134
- // ─────────────────────────────────────────────────────────────────────────────
135
-
136
- interface JobCardProps {
137
- job: string;
138
- stats?: JobStats;
139
- isTriggering: boolean;
140
- onTrigger: (job: string) => void;
141
- onSelect: (job: string) => void;
142
- isSelected: boolean;
143
- }
144
-
145
- const JobCard = (props: JobCardProps) => {
146
- const { job, stats, isTriggering, onTrigger, onSelect, isSelected } = props;
147
- const theme = useMantineTheme();
148
-
149
- const successRate = stats
150
- ? stats.total > 0
151
- ? (stats.completed / stats.total) * 100
152
- : 0
153
- : 0;
154
-
155
- return (
156
- <Card
157
- p="md"
158
- radius="md"
159
- withBorder
160
- onClick={() => onSelect(job)}
161
- style={{
162
- cursor: "pointer",
163
- borderColor: isSelected ? theme.colors.blue[6] : undefined,
164
- backgroundColor: isSelected
165
- ? "var(--mantine-color-blue-light)"
166
- : undefined,
167
- transition: "all 150ms ease",
168
- }}
169
- >
170
- <Group justify="space-between" mb="xs">
171
- <Group gap="xs">
172
- <ThemeIcon
173
- size="sm"
174
- radius="sm"
175
- variant="light"
176
- color={
177
- stats?.lastStatus ? getStatusColor(stats.lastStatus) : "gray"
178
- }
179
- >
180
- <IconTerminal2 size={14} />
181
- </ThemeIcon>
182
- <Text size="sm" fw={600} ff="monospace">
183
- {job}
184
- </Text>
185
- </Group>
186
- <Tooltip label="Trigger job manually" position="left">
187
- <ActionIcon
188
- size="sm"
189
- variant="light"
190
- color="blue"
191
- loading={isTriggering}
192
- onClick={(e) => {
193
- e.stopPropagation();
194
- onTrigger(job);
195
- }}
196
- >
197
- <IconPlayerPlay size={12} />
198
- </ActionIcon>
199
- </Tooltip>
200
- </Group>
201
-
202
- {stats ? (
203
- <>
204
- <Group gap="lg" mb="xs">
205
- <Box>
206
- <Text size="xs" c="dimmed" tt="uppercase" fw={500}>
207
- Total
208
- </Text>
209
- <Text size="lg" fw={700} ff="monospace">
210
- {stats.total}
211
- </Text>
212
- </Box>
213
- <Box>
214
- <Text size="xs" c="dimmed" tt="uppercase" fw={500}>
215
- Success
216
- </Text>
217
- <Text size="lg" fw={700} ff="monospace" c="teal">
218
- {stats.completed}
219
- </Text>
220
- </Box>
221
- <Box>
222
- <Text size="xs" c="dimmed" tt="uppercase" fw={500}>
223
- Failed
224
- </Text>
225
- <Text size="lg" fw={700} ff="monospace" c="red">
226
- {stats.failed}
227
- </Text>
228
- </Box>
229
- </Group>
230
-
231
- <Progress.Root size="sm" radius="xs">
232
- <Tooltip label={`${stats.completed} completed`}>
233
- <Progress.Section
234
- value={(stats.completed / Math.max(stats.total, 1)) * 100}
235
- color="teal"
236
- />
237
- </Tooltip>
238
- <Tooltip label={`${stats.failed} failed`}>
239
- <Progress.Section
240
- value={(stats.failed / Math.max(stats.total, 1)) * 100}
241
- color="red"
242
- />
243
- </Tooltip>
244
- <Tooltip label={`${stats.running} running`}>
245
- <Progress.Section
246
- value={(stats.running / Math.max(stats.total, 1)) * 100}
247
- color="blue"
248
- />
249
- </Tooltip>
250
- </Progress.Root>
251
-
252
- {stats.lastRun && (
253
- <Text size="xs" c="dimmed" mt="xs">
254
- Last run: {formatDuration(stats.lastRun, new Date())} ago
255
- </Text>
256
- )}
257
- </>
258
- ) : (
259
- <Stack gap="xs">
260
- <Skeleton height={8} radius="xl" />
261
- <Skeleton height={8} width="70%" radius="xl" />
262
- </Stack>
263
- )}
264
- </Card>
265
- );
266
- };
267
-
268
- // ─────────────────────────────────────────────────────────────────────────────
269
- // Execution Log Viewer
270
- // ─────────────────────────────────────────────────────────────────────────────
271
-
272
- interface ExecutionLogViewerProps {
273
- logs?: LogEntry[];
274
- error?: string | null;
275
- }
276
-
277
- const ExecutionLogViewer = (props: ExecutionLogViewerProps) => {
278
- const { logs, error } = props;
279
-
280
- if (!logs?.length && !error) {
281
- return (
282
- <Box p="md">
283
- <Text size="sm" c="dimmed" ta="center">
284
- No logs available
285
- </Text>
286
- </Box>
287
- );
288
- }
289
-
290
- return (
291
- <ScrollArea h={300} type="auto">
292
- <Box
293
- p="md"
294
- style={{
295
- fontFamily: "var(--mantine-font-family-monospace)",
296
- fontSize: "12px",
297
- lineHeight: 1.6,
298
- }}
299
- >
300
- {error && (
301
- <Paper p="sm" mb="md" bg="var(--mantine-color-red-light)" radius="sm">
302
- <Group gap="xs" align="flex-start">
303
- <IconAlertTriangle
304
- size={14}
305
- color="var(--mantine-color-red-filled)"
306
- />
307
- <Text
308
- size="xs"
309
- c="red"
310
- style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
311
- >
312
- {error}
313
- </Text>
314
- </Group>
315
- </Paper>
316
- )}
317
-
318
- {logs?.map((log, index) => (
319
- <Group key={index} gap="sm" align="flex-start" mb={4} wrap="nowrap">
320
- <Text size="xs" c="dimmed" style={{ minWidth: 80, flexShrink: 0 }}>
321
- {new Date(log.timestamp).toLocaleTimeString()}
322
- </Text>
323
- <Badge
324
- size="xs"
325
- variant="light"
326
- color={getLogLevelColor(log.level)}
327
- style={{ minWidth: 50 }}
328
- >
329
- {log.level}
330
- </Badge>
331
- <Text size="xs" c="dimmed" style={{ minWidth: 100, flexShrink: 0 }}>
332
- {log.module}
333
- </Text>
334
- <Text size="xs" style={{ wordBreak: "break-word" }}>
335
- {log.message}
336
- </Text>
337
- </Group>
338
- ))}
339
- </Box>
340
- </ScrollArea>
341
- );
342
- };
343
-
344
- // ─────────────────────────────────────────────────────────────────────────────
345
- // Main Component
346
- // ─────────────────────────────────────────────────────────────────────────────
347
-
348
- const AdminJobs = () => {
349
- const client = useClient<AdminJobController>();
350
- const { l } = useI18n();
351
- const toast = useToast();
352
- const dialog = useDialog();
353
-
354
- const [jobs, setJobs] = useState<string[]>([]);
355
- const [jobStats, setJobStats] = useState<Map<string, JobStats>>(new Map());
356
- const [selectedJob, setSelectedJob] = useState<string | null>(null);
357
- const [triggeringJobs, setTriggeringJobs] = useState<Set<string>>(new Set());
358
- const [refreshKey, setRefreshKey] = useState(0);
359
- const [loading, setLoading] = useState(true);
360
- const [activeTab, setActiveTab] = useState<string | null>("overview");
361
-
362
- // Load jobs list
363
- useEffect(() => {
364
- const loadJobs = async () => {
365
- try {
366
- const jobList = await client.getJobs();
367
- setJobs(jobList);
368
-
369
- // Load stats for each job
370
- const statsMap = new Map<string, JobStats>();
371
- for (const job of jobList) {
372
- const executions = await client.getJobExecutions({
373
- query: { job, size: 100 },
374
- });
375
-
376
- const items = executions.content || [];
377
- const completed = items.filter(
378
- (e: JobExecutionEntity) => e.status === "COMPLETED",
379
- ).length;
380
- const failed = items.filter(
381
- (e: JobExecutionEntity) => e.status === "FAILED",
382
- ).length;
383
- const running = items.filter(
384
- (e: JobExecutionEntity) => e.status === "STARTED",
385
- ).length;
386
-
387
- const completedItems = items.filter(
388
- (e: JobExecutionEntity) => e.status === "COMPLETED" && e.finishedAt,
389
- );
390
- const avgDuration =
391
- completedItems.length > 0
392
- ? completedItems.reduce((acc: number, e: JobExecutionEntity) => {
393
- const duration =
394
- new Date(e.finishedAt!).getTime() -
395
- new Date(e.createdAt).getTime();
396
- return acc + duration;
397
- }, 0) / completedItems.length
398
- : 0;
399
-
400
- const lastItem = items[0];
401
-
402
- statsMap.set(job, {
403
- name: job,
404
- total: items.length,
405
- completed,
406
- failed,
407
- running,
408
- avgDuration,
409
- lastRun: lastItem?.createdAt
410
- ? new Date(lastItem.createdAt)
411
- : undefined,
412
- lastStatus: lastItem?.status as JobStats["lastStatus"],
413
- });
414
- }
415
-
416
- setJobStats(statsMap);
417
- } catch (error) {
418
- toast.danger("Failed to load jobs");
419
- } finally {
420
- setLoading(false);
421
- }
422
- };
423
-
424
- loadJobs();
425
- }, [refreshKey]);
426
-
427
- const handleTriggerJob = useCallback(
428
- async (job: string) => {
429
- const confirmed = await dialog.confirm({
430
- title: "Trigger Job",
431
- message: `Are you sure you want to trigger "${job}" manually?`,
432
- confirmLabel: "Trigger",
433
- confirmColor: "blue",
434
- });
435
-
436
- if (!confirmed) return;
437
-
438
- setTriggeringJobs((prev) => new Set(prev).add(job));
439
-
440
- try {
441
- await client.triggerJob({ body: { name: job } });
442
- toast.success(`Job "${job}" triggered successfully`);
443
- setRefreshKey((k) => k + 1);
444
- } catch (error) {
445
- toast.danger(`Failed to trigger job "${job}"`);
446
- } finally {
447
- setTriggeringJobs((prev) => {
448
- const next = new Set(prev);
449
- next.delete(job);
450
- return next;
451
- });
452
- }
453
- },
454
- [client, dialog, toast],
455
- );
456
-
457
- const filters = t.object({
458
- job: t.optional(
459
- t.string({
460
- $control: {
461
- query: t.pick(jobExecutions.schema, ["job"]),
462
- },
463
- }),
464
- ),
465
- status: t.optional(t.enum(["STARTED", "FAILED", "COMPLETED"])),
466
- });
467
-
468
- // Calculate global stats
469
- const globalStats = {
470
- total: Array.from(jobStats.values()).reduce((acc, s) => acc + s.total, 0),
471
- completed: Array.from(jobStats.values()).reduce(
472
- (acc, s) => acc + s.completed,
473
- 0,
474
- ),
475
- failed: Array.from(jobStats.values()).reduce((acc, s) => acc + s.failed, 0),
476
- running: Array.from(jobStats.values()).reduce(
477
- (acc, s) => acc + s.running,
478
- 0,
479
- ),
480
- };
481
-
482
- const successRate =
483
- globalStats.total > 0
484
- ? Math.round((globalStats.completed / globalStats.total) * 100)
485
- : 0;
486
-
487
- return (
488
- <Flex flex={1} direction="column" gap="md">
489
- {/* Header Stats */}
490
- <SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
491
- <Paper p="md" radius="md" withBorder>
492
- <Group justify="space-between">
493
- <Box>
494
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
495
- Total Jobs
496
- </Text>
497
- <Text size="xl" fw={700} ff="monospace">
498
- {jobs.length}
499
- </Text>
500
- </Box>
501
- <ThemeIcon size="lg" radius="md" variant="light" color="blue">
502
- <IconTerminal2 size={20} />
503
- </ThemeIcon>
504
- </Group>
505
- </Paper>
506
-
507
- <Paper p="md" radius="md" withBorder>
508
- <Group justify="space-between">
509
- <Box>
510
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
511
- Executions
512
- </Text>
513
- <Text size="xl" fw={700} ff="monospace">
514
- {globalStats.total}
515
- </Text>
516
- </Box>
517
- <ThemeIcon size="lg" radius="md" variant="light" color="gray">
518
- <IconClock size={20} />
519
- </ThemeIcon>
520
- </Group>
521
- </Paper>
522
-
523
- <Paper p="md" radius="md" withBorder>
524
- <Group justify="space-between">
525
- <Box>
526
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
527
- Success Rate
528
- </Text>
529
- <Text
530
- size="xl"
531
- fw={700}
532
- ff="monospace"
533
- c={
534
- successRate >= 90
535
- ? "teal"
536
- : successRate >= 70
537
- ? "yellow"
538
- : "red"
539
- }
540
- >
541
- {successRate}%
542
- </Text>
543
- </Box>
544
- <RingProgress
545
- size={48}
546
- thickness={4}
547
- roundCaps
548
- sections={[
549
- {
550
- value: successRate,
551
- color:
552
- successRate >= 90
553
- ? "teal"
554
- : successRate >= 70
555
- ? "yellow"
556
- : "red",
557
- },
558
- ]}
559
- />
560
- </Group>
561
- </Paper>
562
-
563
- <Paper p="md" radius="md" withBorder>
564
- <Group justify="space-between">
565
- <Box>
566
- <Text size="xs" c="dimmed" tt="uppercase" fw={600}>
567
- Running Now
568
- </Text>
569
- <Text size="xl" fw={700} ff="monospace" c="blue">
570
- {globalStats.running}
571
- </Text>
572
- </Box>
573
- <ThemeIcon
574
- size="lg"
575
- radius="md"
576
- variant="light"
577
- color={globalStats.running > 0 ? "blue" : "gray"}
578
- >
579
- <IconPlayerPlay size={20} />
580
- </ThemeIcon>
581
- </Group>
582
- </Paper>
583
- </SimpleGrid>
584
-
585
- {/* Tabs */}
586
- <Tabs value={activeTab} onChange={setActiveTab}>
587
- <Tabs.List>
588
- <Tabs.Tab value="overview" leftSection={<IconTerminal2 size={14} />}>
589
- Jobs Overview
590
- </Tabs.Tab>
591
- <Tabs.Tab value="executions" leftSection={<IconClock size={14} />}>
592
- Execution History
593
- </Tabs.Tab>
594
- </Tabs.List>
595
-
596
- <Tabs.Panel value="overview" pt="md">
597
- <Group justify="space-between" mb="md">
598
- <Text size="sm" c="dimmed">
599
- {jobs.length} registered job{jobs.length !== 1 ? "s" : ""}
600
- </Text>
601
- <ActionButton
602
- size="xs"
603
- variant="light"
604
- leftSection={<IconRefresh size={14} />}
605
- onClick={() => setRefreshKey((k) => k + 1)}
606
- >
607
- Refresh
608
- </ActionButton>
609
- </Group>
610
-
611
- {loading ? (
612
- <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
613
- {[1, 2, 3].map((i) => (
614
- <Skeleton key={i} height={150} radius="md" />
615
- ))}
616
- </SimpleGrid>
617
- ) : jobs.length === 0 ? (
618
- <Paper p="xl" radius="md" withBorder ta="center">
619
- <IconTerminal2 size={48} color="var(--mantine-color-dimmed)" />
620
- <Text size="lg" fw={500} mt="md">
621
- No jobs registered
622
- </Text>
623
- <Text size="sm" c="dimmed" mt="xs">
624
- Jobs will appear here once they are defined using $job primitive
625
- </Text>
626
- </Paper>
627
- ) : (
628
- <SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
629
- {jobs.map((job) => (
630
- <JobCard
631
- key={job}
632
- job={job}
633
- stats={jobStats.get(job)}
634
- isTriggering={triggeringJobs.has(job)}
635
- onTrigger={handleTriggerJob}
636
- onSelect={setSelectedJob}
637
- isSelected={selectedJob === job}
638
- />
639
- ))}
640
- </SimpleGrid>
641
- )}
642
- </Tabs.Panel>
643
-
644
- <Tabs.Panel value="executions" pt="md">
645
- <DataTable<JobExecutionEntity, typeof filters>
646
- key={refreshKey}
647
- submitOnInit
648
- defaultSize={15}
649
- typeFormProps={{
650
- skipSubmitButton: true,
651
- columns: 3,
652
- }}
653
- tableProps={{
654
- horizontalSpacing: "sm",
655
- verticalSpacing: "sm",
656
- highlightOnHover: true,
657
- }}
658
- onFilterChange={(key, _value, form) => {
659
- if (key === "job" || key === "status") {
660
- return form.submit();
661
- }
662
- }}
663
- filters={filters}
664
- items={async (filters) => {
665
- const response = await client.getJobExecutions({
666
- query: {
667
- ...filters,
668
- job: selectedJob || filters.job,
669
- },
670
- });
671
-
672
- return response as Page<JobExecutionEntity>;
673
- }}
674
- columns={{
675
- job: {
676
- label: "Job",
677
- value: (item) => (
678
- <Text size="sm" fw={500} ff="monospace">
679
- {item.job}
680
- </Text>
681
- ),
682
- },
683
- status: {
684
- label: "Status",
685
- fit: true,
686
- value: (item) => (
687
- <Badge
688
- size="sm"
689
- variant="light"
690
- color={getStatusColor(item.status)}
691
- leftSection={getStatusIcon(item.status, 12)}
692
- >
693
- {item.status}
694
- </Badge>
695
- ),
696
- },
697
- duration: {
698
- label: "Duration",
699
- fit: true,
700
- value: (item) => (
701
- <Text size="xs" c="dimmed" ff="monospace">
702
- {formatDuration(item.createdAt, item.finishedAt)}
703
- </Text>
704
- ),
705
- },
706
- logs: {
707
- label: "Logs",
708
- fit: true,
709
- value: (item) => {
710
- const logCount =
711
- (item.logs as LogEntry[] | undefined)?.length || 0;
712
- const errorCount =
713
- (item.logs as LogEntry[] | undefined)?.filter(
714
- (log) => log.level === "ERROR",
715
- ).length || 0;
716
-
717
- return (
718
- <Group gap={4}>
719
- <Badge size="xs" variant="light" color="gray">
720
- {logCount} logs
721
- </Badge>
722
- {errorCount > 0 && (
723
- <Badge size="xs" variant="light" color="red">
724
- {errorCount} errors
725
- </Badge>
726
- )}
727
- </Group>
728
- );
729
- },
730
- },
731
- error: {
732
- label: "Error",
733
- value: (item) =>
734
- item.error ? (
735
- <Tooltip label={item.error} multiline w={300}>
736
- <Text size="xs" c="red" lineClamp={1}>
737
- {item.error}
738
- </Text>
739
- </Tooltip>
740
- ) : (
741
- <Text size="xs" c="dimmed">
742
-
743
- </Text>
744
- ),
745
- },
746
- createdAt: {
747
- label: "Started",
748
- fit: true,
749
- value: (item) => (
750
- <Text size="xs" c="dimmed">
751
- {l(item.createdAt, { date: "fromNow" })}
752
- </Text>
753
- ),
754
- },
755
- }}
756
- panel={(item) => (
757
- <Box bg="var(--mantine-color-dark-7)" p={0}>
758
- <ExecutionLogViewer
759
- logs={item.logs as LogEntry[] | undefined}
760
- error={item.error}
761
- />
762
- </Box>
763
- )}
764
- canPanel={(item) => Boolean(item.logs?.length || item.error)}
765
- />
766
- </Tabs.Panel>
767
- </Tabs>
768
- </Flex>
769
- );
770
- };
771
-
772
- export default AdminJobs;