@alepha/ui 0.16.1 → 0.16.2

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 (150) hide show
  1. package/dist/admin/{AdminApiKeys-GMORg-1l.js → AdminApiKeys-CoTOTfgU.js} +4 -3
  2. package/dist/admin/{AdminApiKeys-GMORg-1l.js.map → AdminApiKeys-CoTOTfgU.js.map} +1 -1
  3. package/dist/admin/{AdminAudits-pkWrjq1Z.js → AdminAudits-BmsxFbDa.js} +4 -3
  4. package/dist/admin/{AdminAudits-pkWrjq1Z.js.map → AdminAudits-BmsxFbDa.js.map} +1 -1
  5. package/dist/admin/{AdminFiles-WeQbsCsl.js → AdminFiles-BBB8knca.js} +4 -3
  6. package/dist/admin/{AdminFiles-WeQbsCsl.js.map → AdminFiles-BBB8knca.js.map} +1 -1
  7. package/dist/admin/{AdminJobs-B-q9iGO3.js → AdminJobs-C604joTz.js} +4 -3
  8. package/dist/admin/{AdminJobs-B-q9iGO3.js.map → AdminJobs-C604joTz.js.map} +1 -1
  9. package/dist/admin/{AdminLayout-BqZiXx4H.js → AdminLayout-CsjvpeD1.js} +6 -9
  10. package/dist/admin/AdminLayout-CsjvpeD1.js.map +1 -0
  11. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js → AdminNotifications-LwR6RKrx.js} +4 -3
  12. package/dist/admin/{AdminNotifications-Ds5Un0NJ.js.map → AdminNotifications-LwR6RKrx.js.map} +1 -1
  13. package/dist/admin/AdminParameters-B_83Vie9.js +767 -0
  14. package/dist/admin/AdminParameters-B_83Vie9.js.map +1 -0
  15. package/dist/admin/{AdminSessions-DzIOxM3b.js → AdminSessions-CWnPosdd.js} +4 -3
  16. package/dist/admin/{AdminSessions-DzIOxM3b.js.map → AdminSessions-CWnPosdd.js.map} +1 -1
  17. package/dist/admin/{AdminUserAudits-CiUPN2BC.js → AdminUserAudits-nHv636E_.js} +4 -3
  18. package/dist/admin/{AdminUserAudits-CiUPN2BC.js.map → AdminUserAudits-nHv636E_.js.map} +1 -1
  19. package/dist/admin/{AdminUserCreate-BwQKr4xE.js → AdminUserCreate-CjYD3Kjc.js} +4 -3
  20. package/dist/admin/{AdminUserCreate-BwQKr4xE.js.map → AdminUserCreate-CjYD3Kjc.js.map} +1 -1
  21. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js → AdminUserDetails-Ccq-LsZ0.js} +4 -3
  22. package/dist/admin/{AdminUserDetails-uqtC5aJ1.js.map → AdminUserDetails-Ccq-LsZ0.js.map} +1 -1
  23. package/dist/admin/{AdminUserLayout-CiPay35T.js → AdminUserLayout-7s41DiF_.js} +6 -7
  24. package/dist/admin/AdminUserLayout-7s41DiF_.js.map +1 -0
  25. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js → AdminUserSessions-Ds3ODq_d.js} +4 -3
  26. package/dist/admin/{AdminUserSessions-DAE8Nf1F.js.map → AdminUserSessions-Ds3ODq_d.js.map} +1 -1
  27. package/dist/admin/{AdminUserSettings-EbahaV2a.js → AdminUserSettings-CGh4gROo.js} +4 -3
  28. package/dist/admin/{AdminUserSettings-EbahaV2a.js.map → AdminUserSettings-CGh4gROo.js.map} +1 -1
  29. package/dist/admin/{AdminUsers-Dcjh0KNW.js → AdminUsers-CvPiBzQK.js} +4 -3
  30. package/dist/admin/{AdminUsers-Dcjh0KNW.js.map → AdminUsers-CvPiBzQK.js.map} +1 -1
  31. package/dist/admin/index.d.ts +22 -10
  32. package/dist/admin/index.d.ts.map +1 -1
  33. package/dist/admin/index.js +47 -48
  34. package/dist/admin/index.js.map +1 -1
  35. package/dist/admin/rolldown-runtime-CjeV3_4I.js +18 -0
  36. package/dist/auth/{AuthLayout-Dj5K4SIN.js → AuthLayout-CdJcrPs4.js} +2 -3
  37. package/dist/auth/{AuthLayout-Dj5K4SIN.js.map → AuthLayout-CdJcrPs4.js.map} +1 -1
  38. package/dist/{demo/IconGoogle-CbBF8Hqq.js → auth/IconGoogle-Bm18QD2q.js} +2 -4
  39. package/dist/auth/{IconGoogle-DpSlPZ1u.js.map → IconGoogle-Bm18QD2q.js.map} +1 -1
  40. package/dist/auth/{Login-BBqTosqZ.js → Login-DS_OqA0G.js} +7 -6
  41. package/dist/auth/Login-DS_OqA0G.js.map +1 -0
  42. package/dist/auth/{Profile-Bxj8Nwom.js → Profile-Di7N7HZL.js} +2 -3
  43. package/dist/auth/{Profile-Bxj8Nwom.js.map → Profile-Di7N7HZL.js.map} +1 -1
  44. package/dist/auth/{Register-Ce675Crg.js → Register-BRR2_gux.js} +7 -6
  45. package/dist/auth/Register-BRR2_gux.js.map +1 -0
  46. package/dist/auth/{ResetPassword-DWdt7c40.js → ResetPassword-oQu72lod.js} +4 -3
  47. package/dist/auth/{ResetPassword-DWdt7c40.js.map → ResetPassword-oQu72lod.js.map} +1 -1
  48. package/dist/auth/{VerifyEmail-CI4JwByV.js → VerifyEmail-DC6HPZjd.js} +4 -3
  49. package/dist/auth/{VerifyEmail-CI4JwByV.js.map → VerifyEmail-DC6HPZjd.js.map} +1 -1
  50. package/dist/auth/index.d.ts +14 -14
  51. package/dist/auth/index.d.ts.map +1 -1
  52. package/dist/auth/index.js +13 -13
  53. package/dist/auth/index.js.map +1 -1
  54. package/dist/auth/rolldown-runtime-CjeV3_4I.js +18 -0
  55. package/dist/core/index.d.ts +147 -68
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +349 -287
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/demo/{DemoDataTable-CguplbR7.js → DemoDataTable-DCsJq8v5.js} +4 -5
  60. package/dist/demo/DemoDataTable-DCsJq8v5.js.map +1 -0
  61. package/dist/demo/{DemoHome-Cce2bWmg.js → DemoHome-DpRrPlBC.js} +4 -3
  62. package/dist/demo/{DemoHome-Cce2bWmg.js.map → DemoHome-DpRrPlBC.js.map} +1 -1
  63. package/dist/demo/{DemoJsonViewer-Dgdk3Txb.js → DemoJsonViewer-zeucGKHV.js} +6 -5
  64. package/dist/demo/DemoJsonViewer-zeucGKHV.js.map +1 -0
  65. package/dist/demo/{DemoLayout-B20TEuhV.js → DemoLayout-PhgbAAiQ.js} +6 -5
  66. package/dist/demo/DemoLayout-PhgbAAiQ.js.map +1 -0
  67. package/dist/demo/{DemoLogin-CvCG2WVh.js → DemoLogin-DSzP0Lkv.js} +8 -10
  68. package/dist/demo/DemoLogin-DSzP0Lkv.js.map +1 -0
  69. package/dist/demo/{DemoRegister-CmeHbOAs.js → DemoRegister-DavFBsCz.js} +8 -10
  70. package/dist/demo/DemoRegister-DavFBsCz.js.map +1 -0
  71. package/dist/demo/{DemoResetPassword-CKO5iA_6.js → DemoResetPassword-BS2rIAQK.js} +5 -7
  72. package/dist/demo/DemoResetPassword-BS2rIAQK.js.map +1 -0
  73. package/dist/demo/{DemoSidebar-MVmQKfMt.js → DemoSidebar-zNkUmHRl.js} +4 -5
  74. package/dist/demo/DemoSidebar-zNkUmHRl.js.map +1 -0
  75. package/dist/demo/{DemoTypeForm-w-qtfRlC.js → DemoTypeForm-B9q7oT0b.js} +4 -5
  76. package/dist/demo/DemoTypeForm-B9q7oT0b.js.map +1 -0
  77. package/dist/demo/{DemoVerifyEmail-C8FFJT5A.js → DemoVerifyEmail-Bi4SdWz0.js} +5 -7
  78. package/dist/demo/DemoVerifyEmail-Bi4SdWz0.js.map +1 -0
  79. package/dist/{auth/IconGoogle-DpSlPZ1u.js → demo/IconGoogle-CTeZyrek.js} +2 -4
  80. package/dist/demo/{IconGoogle-CbBF8Hqq.js.map → IconGoogle-CTeZyrek.js.map} +1 -1
  81. package/dist/demo/{Showcase-CQrMWars.js → Showcase-C9btr_SJ.js} +3 -5
  82. package/dist/demo/Showcase-C9btr_SJ.js.map +1 -0
  83. package/dist/demo/index.d.ts +2 -2
  84. package/dist/demo/index.d.ts.map +1 -1
  85. package/dist/demo/index.js +15 -15
  86. package/dist/demo/rolldown-runtime-CjeV3_4I.js +18 -0
  87. package/package.json +5 -3
  88. package/src/admin/AdminRouter.ts +15 -24
  89. package/src/admin/components/AdminLayout.tsx +6 -9
  90. package/src/admin/components/parameters/AdminParameters.tsx +154 -76
  91. package/src/admin/components/parameters/ParameterDetails.tsx +153 -93
  92. package/src/admin/components/parameters/ParameterEmptyState.tsx +27 -0
  93. package/src/admin/components/parameters/ParameterHistory.tsx +15 -20
  94. package/src/admin/components/parameters/ParameterTree.tsx +280 -104
  95. package/src/admin/components/parameters/types.ts +3 -3
  96. package/src/admin/primitives/$uiAdmin.ts +2 -2
  97. package/src/auth/AuthRouter.ts +1 -0
  98. package/src/core/components/buttons/ActionButton.tsx +4 -15
  99. package/src/core/components/buttons/DarkModeButton.tsx +8 -4
  100. package/src/core/components/buttons/ToggleSidebarButton.tsx +3 -5
  101. package/src/core/components/form/Control.tsx +10 -32
  102. package/src/core/components/form/ControlArray.tsx +200 -89
  103. package/src/core/components/form/TypeForm.browser.spec.tsx +727 -0
  104. package/src/core/components/layout/AlephaMantineProvider.tsx +1 -0
  105. package/src/core/components/layout/Breadcrumb.tsx +91 -0
  106. package/src/core/components/layout/{AdminShell.tsx → DashboardShell.tsx} +77 -32
  107. package/src/core/components/layout/Sidebar.tsx +58 -18
  108. package/src/core/constants/ui.ts +1 -1
  109. package/src/core/helpers/renderIcon.tsx +5 -2
  110. package/src/core/index.ts +9 -5
  111. package/src/core/styles.css +7 -7
  112. package/src/core/utils/string.ts +28 -4
  113. package/src/demo/components/DemoLayout.tsx +6 -2
  114. package/dist/admin/AdminApiKeys-DsmGnHNh.js +0 -3
  115. package/dist/admin/AdminAudits-8SM96viT.js +0 -3
  116. package/dist/admin/AdminFiles-B56ocq4H.js +0 -3
  117. package/dist/admin/AdminJobs-CED1syCn.js +0 -3
  118. package/dist/admin/AdminLayout-BqZiXx4H.js.map +0 -1
  119. package/dist/admin/AdminNotifications-B0B1rdc4.js +0 -3
  120. package/dist/admin/AdminParameters-BU3lATdJ.js +0 -3
  121. package/dist/admin/AdminParameters-CfDUpc78.js +0 -575
  122. package/dist/admin/AdminParameters-CfDUpc78.js.map +0 -1
  123. package/dist/admin/AdminSessions-BDGK2MS6.js +0 -3
  124. package/dist/admin/AdminUserAudits-Cj79gENT.js +0 -3
  125. package/dist/admin/AdminUserCreate-Cq-mUmBs.js +0 -3
  126. package/dist/admin/AdminUserDetails-DRjVAPFd.js +0 -3
  127. package/dist/admin/AdminUserLayout-CGzmHHby.js +0 -3
  128. package/dist/admin/AdminUserLayout-CiPay35T.js.map +0 -1
  129. package/dist/admin/AdminUserSessions-DcdzuNZ9.js +0 -3
  130. package/dist/admin/AdminUserSettings-D7V6-ceX.js +0 -3
  131. package/dist/admin/AdminUsers-D9nyzGqQ.js +0 -3
  132. package/dist/auth/Login-BBqTosqZ.js.map +0 -1
  133. package/dist/auth/Login-CoU63mMR.js +0 -4
  134. package/dist/auth/Register-BV_oa_AK.js +0 -4
  135. package/dist/auth/Register-Ce675Crg.js.map +0 -1
  136. package/dist/auth/ResetPassword-D5wC8GAA.js +0 -3
  137. package/dist/auth/VerifyEmail-DAfqVm5s.js +0 -3
  138. package/dist/demo/DemoDataTable-CguplbR7.js.map +0 -1
  139. package/dist/demo/DemoHome-DC9qkMNe.js +0 -3
  140. package/dist/demo/DemoJsonViewer-DIssGVlJ.js +0 -4
  141. package/dist/demo/DemoJsonViewer-Dgdk3Txb.js.map +0 -1
  142. package/dist/demo/DemoLayout-B20TEuhV.js.map +0 -1
  143. package/dist/demo/DemoLayout-DSRyf4qJ.js +0 -3
  144. package/dist/demo/DemoLogin-CvCG2WVh.js.map +0 -1
  145. package/dist/demo/DemoRegister-CmeHbOAs.js.map +0 -1
  146. package/dist/demo/DemoResetPassword-CKO5iA_6.js.map +0 -1
  147. package/dist/demo/DemoSidebar-MVmQKfMt.js.map +0 -1
  148. package/dist/demo/DemoTypeForm-w-qtfRlC.js.map +0 -1
  149. package/dist/demo/DemoVerifyEmail-C8FFJT5A.js.map +0 -1
  150. package/dist/demo/Showcase-CQrMWars.js.map +0 -1
@@ -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, Stack } 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,25 +122,32 @@ 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) {
@@ -100,7 +161,7 @@ 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
167
  </Stack>
@@ -109,26 +170,43 @@ const AdminParameters = () => {
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 { Box, Card, Code, Group, Loader, Stack } 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
+ <Box
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
+ </Box>
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,89 +94,68 @@ 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
+ <Box
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
+ <Box flex={1} p="md" className="overflow-auto" style={{ minHeight: 0 }}>
132
135
  {currentContent !== null ? (
133
- <Stack gap="md">
136
+ <Stack gap="lg">
137
+ {/* Form or JSON view */}
134
138
  <Box>
135
- <Text size="xs" c="dimmed" mb={4}>
136
- Current Value
137
- </Text>
138
139
  {hasValidSchema ? (
139
140
  <TypeForm
140
141
  form={form}
141
- columns={1}
142
+ columns={columns}
142
143
  skipSubmitButton
143
- skipFormElement
144
+ fill={false}
144
145
  />
145
146
  ) : (
146
- <Code block style={{ whiteSpace: "pre-wrap" }}>
147
- {formatJson(currentContent)}
148
- </Code>
147
+ <Box>
148
+ <Text size="xs" c="dimmed" mb={4}>
149
+ Current Value
150
+ </Text>
151
+ <Code block style={{ whiteSpace: "pre-wrap" }}>
152
+ {formatJson(currentContent)}
153
+ </Code>
154
+ </Box>
149
155
  )}
150
156
  </Box>
151
157
 
158
+ {/* Metadata */}
152
159
  {configValue?.current?.changeDescription && (
153
160
  <Box>
154
161
  <Text size="xs" c="dimmed" mb={4}>
@@ -187,15 +194,16 @@ const ParameterDetails = ({
187
194
  </Text>
188
195
  )}
189
196
 
197
+ {/* Scheduled update preview */}
190
198
  {configValue?.next && (
191
- <Card withBorder bg="blue.0" p="sm">
199
+ <Card withBorder p="sm" bg="var(--mantine-color-blue-light)">
192
200
  <Stack gap="xs">
193
201
  <Group gap="xs">
194
202
  <IconClock
195
203
  size={14}
196
204
  color="var(--mantine-color-blue-6)"
197
205
  />
198
- <Text size="xs" fw={500} c="blue.7">
206
+ <Text size="xs" fw={500} c="blue">
199
207
  Scheduled Update (v{configValue.next.version})
200
208
  </Text>
201
209
  </Group>
@@ -219,9 +227,61 @@ const ParameterDetails = ({
219
227
  </Text>
220
228
  </Flex>
221
229
  )}
222
- </ScrollArea>
223
- </Stack>
224
- </Card>
230
+ </Box>
231
+
232
+ {/* Footer with actions */}
233
+ {hasValidSchema && currentContent !== null && (
234
+ <Box
235
+ p="md"
236
+ style={{
237
+ flexShrink: 0,
238
+ borderTop: "1px solid var(--mantine-color-default-border)",
239
+ }}
240
+ >
241
+ <Group justify="flex-end" gap="sm">
242
+ <ActionButton
243
+ variant="subtle"
244
+ onClick={() => form.reset({} as any)}
245
+ disabled={saving}
246
+ >
247
+ Reset
248
+ </ActionButton>
249
+ <ActionButton intent="primary" form={form} loading={saving}>
250
+ Save Changes
251
+ </ActionButton>
252
+ </Group>
253
+ </Box>
254
+ )}
255
+ </Flex>
256
+ </Box>
257
+ );
258
+ };
259
+
260
+ /**
261
+ * Parameter details panel.
262
+ * Shows loading state or the config form.
263
+ * Note: Empty state is handled by parent (AdminParameters).
264
+ */
265
+ const ParameterDetails = ({
266
+ selectedConfig,
267
+ configValue,
268
+ loading,
269
+ saving,
270
+ onSave,
271
+ }: ParameterDetailsProps) => {
272
+ // Loading state
273
+ if (loading) {
274
+ return <LoadingState />;
275
+ }
276
+
277
+ // Config form (selectedConfig is guaranteed to be non-null by parent)
278
+ return (
279
+ <ConfigForm
280
+ selectedConfig={selectedConfig!}
281
+ configValue={configValue}
282
+ saving={saving}
283
+ onSave={onSave}
284
+ />
225
285
  );
226
286
  };
227
287
 
@@ -0,0 +1,27 @@
1
+ import { Text } from "@alepha/ui";
2
+ import { Flex, Stack } from "@mantine/core";
3
+ import { IconArrowLeft } from "@tabler/icons-react";
4
+
5
+ /**
6
+ * Empty state displayed when no parameter is selected.
7
+ * Invites user to select a parameter from the tree.
8
+ */
9
+ const ParameterEmptyState = () => {
10
+ return (
11
+ <Flex flex={1} p={"xl"} align="center">
12
+ <Stack align="center" gap="md">
13
+ <IconArrowLeft size={32} color="var(--mantine-color-dimmed)" />
14
+ <Stack align="center" gap={4}>
15
+ <Text fw={500} c="dimmed">
16
+ No Parameter Selected
17
+ </Text>
18
+ <Text size="xs" c="dimmed" ta="center" maw={240}>
19
+ Choose a parameter from the tree to view and edit its configuration
20
+ </Text>
21
+ </Stack>
22
+ </Stack>
23
+ </Flex>
24
+ );
25
+ };
26
+
27
+ export default ParameterEmptyState;