@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,61 +1,115 @@
1
- import { Flex, Text } from "@alepha/ui";
2
- import { Loader, Stack } from "@mantine/core";
1
+ import { Text, useToast } from "@alepha/ui";
2
+ import { Card, Flex } from "@mantine/core";
3
3
  import { IconSettings } from "@tabler/icons-react";
4
4
  import type {
5
- AdminConfigController,
6
- ConfigTreeNode,
5
+ AdminParameterController,
7
6
  Parameter,
7
+ ParameterTreeNode,
8
8
  } from "alepha/api/parameters";
9
9
  import { useClient } from "alepha/react";
10
10
  import { useCallback, useEffect, useState } from "react";
11
11
  import ParameterDetails from "./ParameterDetails.tsx";
12
+ import ParameterEmptyState from "./ParameterEmptyState.tsx";
12
13
  import ParameterHistory from "./ParameterHistory.tsx";
13
14
  import ParameterTree from "./ParameterTree.tsx";
14
- import type { ConfigValue } from "./types.ts";
15
+ import type { ParameterValue } from "./types.ts";
15
16
 
16
- const AdminParameters = () => {
17
- const client = useClient<AdminConfigController>();
17
+ export interface AdminParametersProps {
18
+ treeData: ParameterTreeNode[];
19
+ }
20
+
21
+ const AdminParameters = ({
22
+ treeData: initialTreeData,
23
+ }: AdminParametersProps) => {
24
+ const client = useClient<AdminParameterController>();
25
+ const toast = useToast();
18
26
 
19
27
  // State
20
- const [treeData, setTreeData] = useState<ConfigTreeNode[]>([]);
28
+ const [treeData, setTreeData] =
29
+ useState<ParameterTreeNode[]>(initialTreeData);
21
30
  const [selectedConfig, setSelectedConfig] = useState<string | null>(null);
22
- const [configValue, setConfigValue] = useState<ConfigValue | null>(null);
31
+ const [configValue, setConfigValue] = useState<ParameterValue | null>(null);
23
32
  const [history, setHistory] = useState<Parameter[]>([]);
24
- const [loading, setLoading] = useState(true);
25
33
  const [loadingConfig, setLoadingConfig] = useState(false);
26
34
  const [loadingHistory, setLoadingHistory] = useState(false);
35
+ const [saving, setSaving] = useState(false);
27
36
 
28
- // Load tree data
29
- const loadTree = useCallback(async () => {
37
+ // Refresh tree data
38
+ const handleRefresh = useCallback(async () => {
30
39
  try {
31
- const tree = await client.getConfigTree({});
32
- setTreeData(tree as ConfigTreeNode[]);
33
- } finally {
34
- setLoading(false);
40
+ const tree = await client.getParameterTree({});
41
+ setTreeData(tree as ParameterTreeNode[]);
42
+ } catch (error) {
43
+ toast.danger({
44
+ title: "Failed to refresh parameters",
45
+ message: error instanceof Error ? error.message : "Unknown error",
46
+ });
35
47
  }
36
- }, []);
48
+ }, [client, toast]);
37
49
 
38
50
  // Load config value and history when selection changes
39
- const loadConfigDetails = useCallback(async (name: string) => {
40
- setLoadingConfig(true);
41
- setLoadingHistory(true);
42
-
43
- try {
44
- const [current] = await Promise.all([
45
- client.getCurrent({ params: { name } }),
46
- ]);
47
- setConfigValue(current);
48
- setHistory([]);
49
- } finally {
50
- setLoadingConfig(false);
51
- setLoadingHistory(false);
52
- }
53
- }, []);
51
+ const loadConfigDetails = useCallback(
52
+ async (name: string) => {
53
+ setLoadingConfig(true);
54
+ setLoadingHistory(true);
55
+
56
+ try {
57
+ const [currentResponse, historyResponse] = await Promise.all([
58
+ client.getCurrent({ params: { name } }),
59
+ client.getHistory({ params: { name } }),
60
+ ]);
61
+ setConfigValue(currentResponse);
62
+ setHistory(historyResponse.versions);
63
+ } catch (error) {
64
+ toast.danger({
65
+ title: "Failed to load configuration",
66
+ message: error instanceof Error ? error.message : "Unknown error",
67
+ });
68
+ setConfigValue(null);
69
+ setHistory([]);
70
+ } finally {
71
+ setLoadingConfig(false);
72
+ setLoadingHistory(false);
73
+ }
74
+ },
75
+ [client, toast],
76
+ );
54
77
 
55
- // Initial load
56
- useEffect(() => {
57
- loadTree();
58
- }, [loadTree]);
78
+ // Handle save
79
+ const handleSave = useCallback(
80
+ async (values: Record<string, unknown>) => {
81
+ if (!selectedConfig || !configValue) return;
82
+
83
+ setSaving(true);
84
+ try {
85
+ await client.createVersion({
86
+ params: { name: selectedConfig },
87
+ body: {
88
+ content: values,
89
+ schemaHash: "", // Schema hash is computed server-side when empty
90
+ changeDescription: "Updated via admin UI",
91
+ },
92
+ });
93
+
94
+ toast.success({
95
+ title: "Configuration saved",
96
+ message: `${selectedConfig} has been updated`,
97
+ });
98
+
99
+ // Reload details
100
+ await loadConfigDetails(selectedConfig);
101
+ } catch (error) {
102
+ toast.danger({
103
+ title: "Failed to save configuration",
104
+ message: error instanceof Error ? error.message : "Unknown error",
105
+ });
106
+ throw error;
107
+ } finally {
108
+ setSaving(false);
109
+ }
110
+ },
111
+ [client, selectedConfig, configValue, loadConfigDetails, toast],
112
+ );
59
113
 
60
114
  // Load details when selection changes
61
115
  useEffect(() => {
@@ -68,31 +122,38 @@ const AdminParameters = () => {
68
122
  }, [selectedConfig, loadConfigDetails]);
69
123
 
70
124
  // Handle rollback
71
- const handleRollback = async (version: number) => {
72
- if (!selectedConfig) return;
73
-
74
- await client.rollback({
75
- params: { name: selectedConfig },
76
- body: { targetVersion: version },
77
- });
78
-
79
- // Reload details
80
- await loadConfigDetails(selectedConfig);
81
- };
82
-
83
- if (loading) {
84
- return (
85
- <Flex flex={1} justify="center" align="center">
86
- <Loader />
87
- </Flex>
88
- );
89
- }
125
+ const handleRollback = useCallback(
126
+ async (version: number) => {
127
+ if (!selectedConfig) return;
128
+
129
+ try {
130
+ await client.rollback({
131
+ params: { name: selectedConfig },
132
+ body: { targetVersion: version },
133
+ });
134
+
135
+ toast.success({
136
+ title: "Rollback successful",
137
+ message: `${selectedConfig} rolled back to version ${version}`,
138
+ });
139
+
140
+ // Reload details
141
+ await loadConfigDetails(selectedConfig);
142
+ } catch (error) {
143
+ toast.danger({
144
+ title: "Rollback failed",
145
+ message: error instanceof Error ? error.message : "Unknown error",
146
+ });
147
+ }
148
+ },
149
+ [client, selectedConfig, loadConfigDetails, toast],
150
+ );
90
151
 
91
152
  // Empty state when no configs exist
92
153
  if (treeData.length === 0) {
93
154
  return (
94
155
  <Flex flex={1} justify="center" align="center">
95
- <Stack align="center" gap="xs">
156
+ <Flex direction="column" align="center" gap="xs">
96
157
  <IconSettings
97
158
  size={48}
98
159
  stroke={1.5}
@@ -100,35 +161,52 @@ const AdminParameters = () => {
100
161
  />
101
162
  <Text c="dimmed">No Parameters Found</Text>
102
163
  <Text size="xs" c="dimmed" ta="center" maw={400}>
103
- Define parameters using the $config primitive to manage dynamic
164
+ Define parameters using the $parameter primitive to manage dynamic
104
165
  application settings. Parameters will appear here once created.
105
166
  </Text>
106
- </Stack>
167
+ </Flex>
107
168
  </Flex>
108
169
  );
109
170
  }
110
171
 
111
172
  return (
112
- <Flex flex={1} gap={"xs"} h="100%">
113
- <ParameterTree
114
- treeData={treeData}
115
- selectedConfig={selectedConfig}
116
- onSelect={setSelectedConfig}
117
- onRefresh={loadTree}
118
- />
119
-
120
- <ParameterDetails
121
- selectedConfig={selectedConfig}
122
- configValue={configValue}
123
- loading={loadingConfig}
124
- />
125
-
126
- <ParameterHistory
127
- selectedConfig={selectedConfig}
128
- history={history}
129
- loading={loadingHistory}
130
- onRollback={handleRollback}
131
- />
173
+ <Flex flex={1} p="md">
174
+ <Card
175
+ withBorder
176
+ p={0}
177
+ w={"100%"}
178
+ style={{
179
+ flexDirection: "row",
180
+ }}
181
+ >
182
+ <ParameterTree
183
+ treeData={treeData}
184
+ selectedConfig={selectedConfig}
185
+ onSelect={setSelectedConfig}
186
+ onRefresh={handleRefresh}
187
+ />
188
+
189
+ {selectedConfig ? (
190
+ <>
191
+ <ParameterDetails
192
+ selectedConfig={selectedConfig}
193
+ configValue={configValue}
194
+ loading={loadingConfig}
195
+ saving={saving}
196
+ onSave={handleSave}
197
+ />
198
+
199
+ <ParameterHistory
200
+ selectedConfig={selectedConfig}
201
+ history={history}
202
+ loading={loadingHistory}
203
+ onRollback={handleRollback}
204
+ />
205
+ </>
206
+ ) : (
207
+ <ParameterEmptyState />
208
+ )}
209
+ </Card>
132
210
  </Flex>
133
211
  );
134
212
  };
@@ -1,32 +1,56 @@
1
- import { Flex, Text, TypeForm } from "@alepha/ui";
2
- import {
3
- Badge,
4
- Box,
5
- Card,
6
- Code,
7
- Group,
8
- Loader,
9
- ScrollArea,
10
- Stack,
11
- } from "@mantine/core";
12
- import { IconClock, IconSettings } from "@tabler/icons-react";
13
- import { jsonSchemaToTypeBox, type TObject } from "alepha";
1
+ import { ActionButton, Flex, Text, TypeForm } from "@alepha/ui";
2
+ import { Card, Code, Loader } from "@mantine/core";
3
+ import { IconClock } from "@tabler/icons-react";
4
+ import { jsonSchemaToTypeBox, type TObject, t } from "alepha";
14
5
  import { useForm } from "alepha/react/form";
15
6
  import { useI18n } from "alepha/react/i18n";
16
7
  import { useMemo } from "react";
17
- import { type ConfigValue, formatJson } from "./types.ts";
8
+ import { formatJson, type ParameterValue } from "./types.ts";
18
9
 
19
10
  export interface ParameterDetailsProps {
20
11
  selectedConfig: string | null;
21
- configValue: ConfigValue | null;
12
+ configValue: ParameterValue | null;
22
13
  loading: boolean;
14
+ saving: boolean;
15
+ onSave: (values: Record<string, unknown>) => Promise<void>;
23
16
  }
24
17
 
25
- const ParameterDetails = ({
18
+ /**
19
+ * Loading state.
20
+ */
21
+ const LoadingState = () => (
22
+ <Flex
23
+ flex={1}
24
+ h="100%"
25
+ p="md"
26
+ style={{
27
+ overflow: "hidden",
28
+ minWidth: 0,
29
+ display: "flex",
30
+ }}
31
+ >
32
+ <Flex flex={1} justify="center" align="center" h="100%">
33
+ <Loader size="sm" />
34
+ </Flex>
35
+ </Flex>
36
+ );
37
+
38
+ interface ConfigFormProps {
39
+ selectedConfig: string;
40
+ configValue: ParameterValue | null;
41
+ saving: boolean;
42
+ onSave: (values: Record<string, unknown>) => Promise<void>;
43
+ }
44
+
45
+ /**
46
+ * The actual form component - only rendered when a config is selected.
47
+ */
48
+ const ConfigForm = ({
26
49
  selectedConfig,
27
50
  configValue,
28
- loading,
29
- }: ParameterDetailsProps) => {
51
+ saving,
52
+ onSave,
53
+ }: ConfigFormProps) => {
30
54
  const { l } = useI18n();
31
55
 
32
56
  // Get the current value to display (from saved version or default)
@@ -43,17 +67,21 @@ const ParameterDetails = ({
43
67
  // Convert JSON Schema from API to TypeBox schema
44
68
  const schemaForForm = useMemo(() => {
45
69
  if (!configValue?.schema) {
46
- return { type: "object", properties: {} } as unknown as TObject;
70
+ return t.object({});
71
+ }
72
+ try {
73
+ return jsonSchemaToTypeBox(configValue.schema) as TObject;
74
+ } catch {
75
+ return t.object({});
47
76
  }
48
- return jsonSchemaToTypeBox(configValue.schema) as TObject;
49
77
  }, [configValue?.schema]);
50
78
 
51
79
  const form = useForm(
52
80
  {
53
81
  schema: schemaForForm,
54
82
  initialValues: (currentContent ?? {}) as Record<string, unknown>,
55
- handler: async () => {
56
- // Read-only for now
83
+ handler: async (values) => {
84
+ await onSave(values as Record<string, unknown>);
57
85
  },
58
86
  },
59
87
  [selectedConfig, schemaForForm, currentContent],
@@ -66,117 +94,101 @@ const ParameterDetails = ({
66
94
  schema &&
67
95
  typeof schema === "object" &&
68
96
  "properties" in schema &&
69
- Object.keys(schema.properties ?? {}).length > 0
97
+ Object.keys(schema.properties as object).length > 0
70
98
  );
71
99
  }, [configValue?.schema]);
72
100
 
73
- if (!selectedConfig) {
74
- return (
75
- <Card withBorder flex={1} h="100%" style={{ overflow: "hidden" }}>
76
- <Flex flex={1} justify="center" align="center" h="100%">
77
- <Stack align="center" gap="xs">
78
- <IconSettings
79
- size={32}
80
- stroke={1.5}
81
- color="var(--mantine-color-dimmed)"
82
- />
83
- <Text c="dimmed" size="sm">
84
- Select a parameter to view its value
85
- </Text>
86
- </Stack>
87
- </Flex>
88
- </Card>
89
- );
90
- }
101
+ // Count the number of fields to determine column layout
102
+ const fieldCount = useMemo(() => {
103
+ const schema = configValue?.schema;
104
+ if (
105
+ schema &&
106
+ typeof schema === "object" &&
107
+ "properties" in schema &&
108
+ schema.properties
109
+ ) {
110
+ return Object.keys(schema.properties as object).length;
111
+ }
112
+ return 0;
113
+ }, [configValue?.schema]);
91
114
 
92
- if (loading) {
93
- return (
94
- <Card withBorder flex={1} h="100%" style={{ overflow: "hidden" }}>
95
- <Flex flex={1} justify="center" align="center" h="100%">
96
- <Loader size="sm" />
97
- </Flex>
98
- </Card>
99
- );
100
- }
115
+ // Determine optimal column count based on field count
116
+ const columns = useMemo(() => {
117
+ if (fieldCount <= 2) return 1;
118
+ if (fieldCount <= 6) return 2;
119
+ return 3;
120
+ }, [fieldCount]);
101
121
 
102
122
  return (
103
- <Card withBorder flex={1} h="100%" style={{ overflow: "hidden" }}>
104
- <Stack gap="md" h="100%">
105
- <Group justify="space-between">
106
- <Stack gap={2}>
107
- <Text size="sm" fw={500}>
108
- {selectedConfig}
109
- </Text>
110
- {configValue?.current && (
111
- <Group gap="xs">
112
- <Badge size="xs" color="green" variant="light">
113
- v{configValue.current.version}
114
- </Badge>
115
- {configValue.next && (
116
- <Badge size="xs" color="blue" variant="light">
117
- Next: v{configValue.next.version}
118
- </Badge>
119
- )}
120
- </Group>
121
- )}
122
- {!configValue?.current &&
123
- configValue?.currentValue !== undefined && (
124
- <Badge size="xs" color="yellow" variant="light">
125
- Default
126
- </Badge>
127
- )}
128
- </Stack>
129
- </Group>
130
-
131
- <ScrollArea flex={1} offsetScrollbars>
123
+ <Flex
124
+ flex={1}
125
+ h="100%"
126
+ style={{
127
+ overflow: "hidden",
128
+ minWidth: 0,
129
+ display: "flex",
130
+ }}
131
+ >
132
+ <Flex direction="column" h="100%" w="100%" style={{ minHeight: 0 }}>
133
+ {/* Content */}
134
+ <Flex
135
+ flex={1}
136
+ p="md"
137
+ className="overflow-auto"
138
+ style={{ minHeight: 0 }}
139
+ >
132
140
  {currentContent !== null ? (
133
- <Stack gap="md">
134
- <Box>
135
- <Text size="xs" c="dimmed" mb={4}>
136
- Current Value
137
- </Text>
141
+ <Flex direction="column" gap="lg">
142
+ {/* Form or JSON view */}
143
+ <Flex>
138
144
  {hasValidSchema ? (
139
145
  <TypeForm
140
146
  form={form}
141
- columns={1}
147
+ columns={columns}
142
148
  skipSubmitButton
143
- skipFormElement
149
+ fill={false}
144
150
  />
145
151
  ) : (
146
- <Code block style={{ whiteSpace: "pre-wrap" }}>
147
- {formatJson(currentContent)}
148
- </Code>
152
+ <Flex>
153
+ <Text size="xs" c="dimmed" mb={4}>
154
+ Current Value
155
+ </Text>
156
+ <Code block style={{ whiteSpace: "pre-wrap" }}>
157
+ {formatJson(currentContent)}
158
+ </Code>
159
+ </Flex>
149
160
  )}
150
- </Box>
161
+ </Flex>
151
162
 
163
+ {/* Metadata */}
152
164
  {configValue?.current?.changeDescription && (
153
- <Box>
165
+ <Flex>
154
166
  <Text size="xs" c="dimmed" mb={4}>
155
167
  Change Description
156
168
  </Text>
157
169
  <Text size="sm">{configValue.current.changeDescription}</Text>
158
- </Box>
170
+ </Flex>
159
171
  )}
160
172
 
161
173
  {configValue?.current && (
162
- <Group gap="xl">
163
- <Box>
174
+ <Flex gap="xl">
175
+ <Flex>
164
176
  <Text size="xs" c="dimmed" mb={2}>
165
177
  Updated
166
178
  </Text>
167
179
  <Text size="sm">
168
180
  {l(configValue.current.updatedAt, { date: "fromNow" })}
169
181
  </Text>
170
- </Box>
182
+ </Flex>
171
183
  {configValue.current.creatorName && (
172
- <Box>
184
+ <Flex>
173
185
  <Text size="xs" c="dimmed" mb={2}>
174
186
  Updated By
175
187
  </Text>
176
188
  <Text size="sm">{configValue.current.creatorName}</Text>
177
- </Box>
189
+ </Flex>
178
190
  )}
179
- </Group>
191
+ </Flex>
180
192
  )}
181
193
 
182
194
  {!configValue?.current &&
@@ -187,18 +199,19 @@ const ParameterDetails = ({
187
199
  </Text>
188
200
  )}
189
201
 
202
+ {/* Scheduled update preview */}
190
203
  {configValue?.next && (
191
- <Card withBorder bg="blue.0" p="sm">
192
- <Stack gap="xs">
193
- <Group gap="xs">
204
+ <Card withBorder p="sm" bg="var(--mantine-color-blue-light)">
205
+ <Flex direction="column" gap="xs">
206
+ <Flex gap="xs">
194
207
  <IconClock
195
208
  size={14}
196
209
  color="var(--mantine-color-blue-6)"
197
210
  />
198
- <Text size="xs" fw={500} c="blue.7">
211
+ <Text size="xs" fw={500} c="blue">
199
212
  Scheduled Update (v{configValue.next.version})
200
213
  </Text>
201
- </Group>
214
+ </Flex>
202
215
  <Text size="xs" c="dimmed">
203
216
  Activates{" "}
204
217
  {l(configValue.next.activationDate, {
@@ -208,10 +221,10 @@ const ParameterDetails = ({
208
221
  <Code block style={{ whiteSpace: "pre-wrap" }} fz="xs">
209
222
  {formatJson(configValue.next.content)}
210
223
  </Code>
211
- </Stack>
224
+ </Flex>
212
225
  </Card>
213
226
  )}
214
- </Stack>
227
+ </Flex>
215
228
  ) : (
216
229
  <Flex justify="center" align="center" h={200}>
217
230
  <Text c="dimmed" size="sm">
@@ -219,9 +232,61 @@ const ParameterDetails = ({
219
232
  </Text>
220
233
  </Flex>
221
234
  )}
222
- </ScrollArea>
223
- </Stack>
224
- </Card>
235
+ </Flex>
236
+
237
+ {/* Footer with actions */}
238
+ {hasValidSchema && currentContent !== null && (
239
+ <Flex
240
+ p="md"
241
+ style={{
242
+ flexShrink: 0,
243
+ borderTop: "1px solid var(--mantine-color-default-border)",
244
+ }}
245
+ >
246
+ <Flex justify="flex-end" gap="sm">
247
+ <ActionButton
248
+ variant="subtle"
249
+ onClick={() => form.reset({} as any)}
250
+ disabled={saving}
251
+ >
252
+ Reset
253
+ </ActionButton>
254
+ <ActionButton intent="primary" form={form} loading={saving}>
255
+ Save Changes
256
+ </ActionButton>
257
+ </Flex>
258
+ </Flex>
259
+ )}
260
+ </Flex>
261
+ </Flex>
262
+ );
263
+ };
264
+
265
+ /**
266
+ * Parameter details panel.
267
+ * Shows loading state or the config form.
268
+ * Note: Empty state is handled by parent (AdminParameters).
269
+ */
270
+ const ParameterDetails = ({
271
+ selectedConfig,
272
+ configValue,
273
+ loading,
274
+ saving,
275
+ onSave,
276
+ }: ParameterDetailsProps) => {
277
+ // Loading state
278
+ if (loading) {
279
+ return <LoadingState />;
280
+ }
281
+
282
+ // Config form (selectedConfig is guaranteed to be non-null by parent)
283
+ return (
284
+ <ConfigForm
285
+ selectedConfig={selectedConfig!}
286
+ configValue={configValue}
287
+ saving={saving}
288
+ onSave={onSave}
289
+ />
225
290
  );
226
291
  };
227
292