@alepha/ui 0.18.0 → 0.18.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 (187) hide show
  1. package/dist/admin/{AdminApiKeys-C-6_Q-lH.js → AdminApiKeys-BJhIwfD6.js} +17 -38
  2. package/dist/admin/AdminApiKeys-BJhIwfD6.js.map +1 -0
  3. package/dist/admin/{AdminAudits-Bgbf04hO.js → AdminAudits-DzD_4cDt.js} +23 -19
  4. package/dist/admin/AdminAudits-DzD_4cDt.js.map +1 -0
  5. package/dist/admin/AdminDashboard-C92tIc6x.js +67 -0
  6. package/dist/admin/AdminDashboard-C92tIc6x.js.map +1 -0
  7. package/dist/admin/{AdminFiles-B9a7G3cY.js → AdminFiles-DLpfhBkf.js} +3 -7
  8. package/dist/admin/AdminFiles-DLpfhBkf.js.map +1 -0
  9. package/dist/admin/{AdminJobDashboard-DaTwf5OY.js → AdminJobDashboard-KIOkeMgE.js} +2 -2
  10. package/dist/admin/{AdminJobDashboard-DaTwf5OY.js.map → AdminJobDashboard-KIOkeMgE.js.map} +1 -1
  11. package/dist/admin/{AdminJobExecutions-B9cek5dl.js → AdminJobExecutions-D0Yo_PU0.js} +24 -36
  12. package/dist/admin/AdminJobExecutions-D0Yo_PU0.js.map +1 -0
  13. package/dist/admin/{AdminJobRegistry-DFgV3oqx.js → AdminJobRegistry-PFajqaGK.js} +10 -18
  14. package/dist/admin/AdminJobRegistry-PFajqaGK.js.map +1 -0
  15. package/dist/admin/AdminLayout-B1DXZHDn.js +61 -0
  16. package/dist/admin/AdminLayout-B1DXZHDn.js.map +1 -0
  17. package/dist/admin/{AdminParameters-DHw9ATgl.js → AdminParameters-BspPeqp_.js} +2 -2
  18. package/dist/admin/{AdminParameters-DHw9ATgl.js.map → AdminParameters-BspPeqp_.js.map} +1 -1
  19. package/dist/admin/{AdminSessions-BhGJPI3z.js → AdminSessions-BnH5CZQl.js} +48 -53
  20. package/dist/admin/AdminSessions-BnH5CZQl.js.map +1 -0
  21. package/dist/admin/{AdminUserLayout-BdC4Te8m.js → AdminUserLayout-DUbC6-BI.js} +2 -2
  22. package/dist/admin/{AdminUserLayout-BdC4Te8m.js.map → AdminUserLayout-DUbC6-BI.js.map} +1 -1
  23. package/dist/admin/{AdminUserProfile-DAt23fqY.js → AdminUserProfile-DuTUnjdG.js} +3 -3
  24. package/dist/admin/{AdminUserProfile-DAt23fqY.js.map → AdminUserProfile-DuTUnjdG.js.map} +1 -1
  25. package/dist/admin/{AdminUserSessions-1uzcx02z.js → AdminUserSessions-DvZdAGpL.js} +33 -35
  26. package/dist/admin/AdminUserSessions-DvZdAGpL.js.map +1 -0
  27. package/dist/admin/AdminUsers-CR9z0g_5.js +206 -0
  28. package/dist/admin/AdminUsers-CR9z0g_5.js.map +1 -0
  29. package/dist/admin/{AuthLayout-DFJvCvzw.js → AuthLayout-DsUfp9RG.js} +2 -2
  30. package/dist/admin/{AuthLayout-DFJvCvzw.js.map → AuthLayout-DsUfp9RG.js.map} +1 -1
  31. package/dist/admin/{IconGoogle-CSQLPYwX.js → IconGoogle-Ch1m3Uzl.js} +1 -1
  32. package/dist/admin/{IconGoogle-CSQLPYwX.js.map → IconGoogle-Ch1m3Uzl.js.map} +1 -1
  33. package/dist/admin/{Login-BGheURrg.js → Login-DHbYJKwg.js} +3 -3
  34. package/dist/{auth/Login-Denw_UGy.js.map → admin/Login-DHbYJKwg.js.map} +1 -1
  35. package/dist/{auth/Profile-BMX_Ar_s.js → admin/Profile-B2EcIDB9.js} +2 -2
  36. package/dist/{auth/Profile-BMX_Ar_s.js.map → admin/Profile-B2EcIDB9.js.map} +1 -1
  37. package/dist/admin/{Register-Cs10l8vX.js → Register-Z3fxRbUF.js} +3 -3
  38. package/dist/{demo/Register-a70LPgs2.js.map → admin/Register-Z3fxRbUF.js.map} +1 -1
  39. package/dist/admin/{ResetPassword-BwDdfkGH.js → ResetPassword-_Y1qTTKh.js} +2 -2
  40. package/dist/admin/{ResetPassword-BwDdfkGH.js.map → ResetPassword-_Y1qTTKh.js.map} +1 -1
  41. package/dist/admin/{VerifyEmail-DfXHAiQl.js → VerifyEmail-Bg22bwcC.js} +2 -2
  42. package/dist/admin/{VerifyEmail-DfXHAiQl.js.map → VerifyEmail-Bg22bwcC.js.map} +1 -1
  43. package/dist/admin/{core-2xoLiT0o.js → core-BVO_TQxb.js} +1474 -233
  44. package/dist/admin/core-BVO_TQxb.js.map +1 -0
  45. package/dist/admin/index.d.ts +29 -4
  46. package/dist/admin/index.d.ts.map +1 -1
  47. package/dist/admin/index.js +448 -69
  48. package/dist/admin/index.js.map +1 -1
  49. package/dist/auth/{AuthLayout-CAE1pX9s.js → AuthLayout-C161NeF6.js} +2 -2
  50. package/dist/auth/{AuthLayout-CAE1pX9s.js.map → AuthLayout-C161NeF6.js.map} +1 -1
  51. package/dist/auth/{Login-Denw_UGy.js → Login-C7jIqf00.js} +2 -2
  52. package/dist/{admin/Login-BGheURrg.js.map → auth/Login-C7jIqf00.js.map} +1 -1
  53. package/dist/{admin/Profile-B-c9pCPf.js → auth/Profile-BMpXJ0oi.js} +2 -2
  54. package/dist/{demo/Profile-CWqti7FB.js.map → auth/Profile-BMpXJ0oi.js.map} +1 -1
  55. package/dist/auth/{Register-6hi_cpfF.js → Register-2gx8qll-.js} +2 -2
  56. package/dist/auth/{Register-6hi_cpfF.js.map → Register-2gx8qll-.js.map} +1 -1
  57. package/dist/{demo/ResetPassword-DWN0lzr5.js → auth/ResetPassword-DBxt9hKk.js} +2 -2
  58. package/dist/auth/{ResetPassword-CqfTk1FI.js.map → ResetPassword-DBxt9hKk.js.map} +1 -1
  59. package/dist/{demo/VerifyEmail-DZWL72K4.js → auth/VerifyEmail-Z80Ubajk.js} +2 -2
  60. package/dist/auth/{VerifyEmail-nWiSTMjF.js.map → VerifyEmail-Z80Ubajk.js.map} +1 -1
  61. package/dist/auth/{core-niW0sFLv.js → core-DyfeVr5c.js} +1002 -38
  62. package/dist/auth/core-DyfeVr5c.js.map +1 -0
  63. package/dist/auth/index.d.ts +12 -1
  64. package/dist/auth/index.d.ts.map +1 -1
  65. package/dist/auth/index.js +12 -13
  66. package/dist/auth/index.js.map +1 -1
  67. package/dist/core/index.d.ts +95 -14
  68. package/dist/core/index.d.ts.map +1 -1
  69. package/dist/core/index.js +1473 -232
  70. package/dist/core/index.js.map +1 -1
  71. package/dist/demo/{AuthLayout-jLa0aKsI.js → AuthLayout-DN-ClJQk.js} +2 -2
  72. package/dist/demo/{AuthLayout-jLa0aKsI.js.map → AuthLayout-DN-ClJQk.js.map} +1 -1
  73. package/dist/demo/{DemoButton-BmaWZVwf.js → DemoButton-CGUyR9eM.js} +3 -3
  74. package/dist/demo/{DemoButton-BmaWZVwf.js.map → DemoButton-CGUyR9eM.js.map} +1 -1
  75. package/dist/demo/{DemoDataTable-Z9xyV221.js → DemoDataTable-QFG-xXSx.js} +15 -19
  76. package/dist/demo/DemoDataTable-QFG-xXSx.js.map +1 -0
  77. package/dist/demo/{DemoDialog-4ItHLf9t.js → DemoDialog-DW8QEvD1.js} +2 -2
  78. package/dist/demo/{DemoDialog-4ItHLf9t.js.map → DemoDialog-DW8QEvD1.js.map} +1 -1
  79. package/dist/demo/{DemoFlex-EtVq8QfX.js → DemoFlex-CAhLUanT.js} +3 -3
  80. package/dist/demo/{DemoFlex-EtVq8QfX.js.map → DemoFlex-CAhLUanT.js.map} +1 -1
  81. package/dist/demo/{DemoHeading-BS-vGfkI.js → DemoHeading-yIFmNjHB.js} +3 -3
  82. package/dist/demo/{DemoHeading-BS-vGfkI.js.map → DemoHeading-yIFmNjHB.js.map} +1 -1
  83. package/dist/demo/{DemoHome-Clbn8AmS.js → DemoHome-BSGuBHus.js} +2 -2
  84. package/dist/demo/{DemoHome-Clbn8AmS.js.map → DemoHome-BSGuBHus.js.map} +1 -1
  85. package/dist/demo/{DemoJsonViewer-DkIX_ky2.js → DemoJsonViewer-DsA2IpgV.js} +3 -3
  86. package/dist/demo/{DemoJsonViewer-DkIX_ky2.js.map → DemoJsonViewer-DsA2IpgV.js.map} +1 -1
  87. package/dist/demo/{DemoLayout-C56xb5EE.js → DemoLayout-Cy6xjn6P.js} +2 -2
  88. package/dist/demo/{DemoLayout-C56xb5EE.js.map → DemoLayout-Cy6xjn6P.js.map} +1 -1
  89. package/dist/demo/{DemoLogin-BZwpicOS.js → DemoLogin-vqxgTu4P.js} +8 -8
  90. package/dist/demo/{DemoLogin-BZwpicOS.js.map → DemoLogin-vqxgTu4P.js.map} +1 -1
  91. package/dist/demo/{DemoRegister-C7_qc4MJ.js → DemoRegister-YHPvPg77.js} +8 -8
  92. package/dist/demo/{DemoRegister-C7_qc4MJ.js.map → DemoRegister-YHPvPg77.js.map} +1 -1
  93. package/dist/demo/{DemoResetPassword-BI1Ct4Dw.js → DemoResetPassword-mOW18Zlm.js} +8 -8
  94. package/dist/demo/{DemoResetPassword-BI1Ct4Dw.js.map → DemoResetPassword-mOW18Zlm.js.map} +1 -1
  95. package/dist/demo/{DemoSidebar-CcBo4ltC.js → DemoSidebar-od7aLjP_.js} +3 -3
  96. package/dist/demo/{DemoSidebar-CcBo4ltC.js.map → DemoSidebar-od7aLjP_.js.map} +1 -1
  97. package/dist/demo/{DemoText-CzXuUn3g.js → DemoText-DU3JeRS0.js} +3 -3
  98. package/dist/demo/{DemoText-CzXuUn3g.js.map → DemoText-DU3JeRS0.js.map} +1 -1
  99. package/dist/demo/{DemoToast-BgHDhWrX.js → DemoToast-CUJEiPRa.js} +2 -2
  100. package/dist/demo/{DemoToast-BgHDhWrX.js.map → DemoToast-CUJEiPRa.js.map} +1 -1
  101. package/dist/demo/{DemoTypeForm-DDzWoMSV.js → DemoTypeForm-C1dNkahD.js} +3 -3
  102. package/dist/demo/{DemoTypeForm-DDzWoMSV.js.map → DemoTypeForm-C1dNkahD.js.map} +1 -1
  103. package/dist/demo/{DemoVerifyEmail-C_Irdnov.js → DemoVerifyEmail-D9EcXZ38.js} +8 -8
  104. package/dist/demo/{DemoVerifyEmail-C_Irdnov.js.map → DemoVerifyEmail-D9EcXZ38.js.map} +1 -1
  105. package/dist/demo/{Login-hSOU3jZc.js → Login-CoYf_P_F.js} +2 -2
  106. package/dist/demo/{Login-hSOU3jZc.js.map → Login-CoYf_P_F.js.map} +1 -1
  107. package/dist/demo/{Profile-CWqti7FB.js → Profile-BE_Y3co2.js} +2 -2
  108. package/dist/{admin/Profile-B-c9pCPf.js.map → demo/Profile-BE_Y3co2.js.map} +1 -1
  109. package/dist/demo/{Register-a70LPgs2.js → Register-fXHmBpr3.js} +2 -2
  110. package/dist/{admin/Register-Cs10l8vX.js.map → demo/Register-fXHmBpr3.js.map} +1 -1
  111. package/dist/{auth/ResetPassword-CqfTk1FI.js → demo/ResetPassword-CAPj8MO3.js} +2 -2
  112. package/dist/demo/{ResetPassword-DWN0lzr5.js.map → ResetPassword-CAPj8MO3.js.map} +1 -1
  113. package/dist/demo/{Showcase-Dq3MISpd.js → Showcase-BtEU0pY9.js} +2 -2
  114. package/dist/demo/{Showcase-Dq3MISpd.js.map → Showcase-BtEU0pY9.js.map} +1 -1
  115. package/dist/{auth/VerifyEmail-nWiSTMjF.js → demo/VerifyEmail-DFmdCdYs.js} +2 -2
  116. package/dist/demo/{VerifyEmail-DZWL72K4.js.map → VerifyEmail-DFmdCdYs.js.map} +1 -1
  117. package/dist/demo/{auth-d6n3xbug.js → auth-Djd7SKiw.js} +8 -8
  118. package/dist/demo/{auth-d6n3xbug.js.map → auth-Djd7SKiw.js.map} +1 -1
  119. package/dist/demo/{core-RCUw1Q-a.js → core-B7LNjM78.js} +1484 -226
  120. package/dist/demo/core-B7LNjM78.js.map +1 -0
  121. package/dist/demo/index.js +17 -17
  122. package/package.json +4 -4
  123. package/src/admin/{AdminRouter.ts → AdminRouter.tsx} +128 -19
  124. package/src/admin/components/AdminDashboard.tsx +52 -0
  125. package/src/admin/components/AdminLayout.tsx +32 -40
  126. package/src/admin/components/audits/AdminAudits.tsx +22 -16
  127. package/src/admin/components/files/AdminFiles.tsx +1 -6
  128. package/src/admin/components/jobs/AdminJobExecutions.tsx +33 -39
  129. package/src/admin/components/jobs/AdminJobRegistry.tsx +9 -18
  130. package/src/admin/components/keys/AdminApiKeys.tsx +23 -41
  131. package/src/admin/components/sessions/AdminSessions.tsx +71 -71
  132. package/src/admin/components/users/AdminUserSessions.tsx +33 -31
  133. package/src/admin/components/users/AdminUsers.tsx +184 -72
  134. package/src/admin/index.ts +2 -2
  135. package/src/admin/primitives/$uiAdmin.ts +1 -1
  136. package/src/auth/components/buttons/UserButton.tsx +1 -3
  137. package/src/core/atoms/alephaSidebarAtom.ts +1 -1
  138. package/src/core/atoms/alephaThemeListAtom.ts +14 -1
  139. package/src/core/atoms/alephaThemeOverridesAtom.ts +17 -0
  140. package/src/core/atoms/themes/editorial.ts +184 -0
  141. package/src/core/atoms/themes/monochrome.ts +197 -0
  142. package/src/core/atoms/themes/rosePine.ts +208 -0
  143. package/src/core/atoms/themes/softBrutalism.ts +221 -0
  144. package/src/core/atoms/themes/terminal.ts +186 -0
  145. package/src/core/components/Flex.tsx +91 -1
  146. package/src/core/components/Text.tsx +1 -1
  147. package/src/core/components/buttons/ActionButton.tsx +15 -19
  148. package/src/core/components/buttons/DarkModeButton.tsx +3 -3
  149. package/src/core/components/buttons/LanguageButton.tsx +1 -1
  150. package/src/core/components/buttons/OmnibarButton.tsx +1 -2
  151. package/src/core/components/buttons/ThemeButton.tsx +40 -11
  152. package/src/core/components/buttons/ThemeExpertModal.tsx +184 -0
  153. package/src/core/components/buttons/ToggleSidebarButton.tsx +1 -2
  154. package/src/core/components/layout/AppBar.tsx +10 -0
  155. package/src/core/components/layout/DashboardShell.tsx +10 -7
  156. package/src/core/components/layout/Sidebar.tsx +60 -52
  157. package/src/core/constants/ui.ts +5 -5
  158. package/src/core/hooks/useTheme.ts +26 -3
  159. package/src/core/index.ts +6 -1
  160. package/src/core/interfaces/AlephaTheme.ts +2 -0
  161. package/src/core/providers/ThemeProvider.ts +108 -8
  162. package/src/core/services/DialogService.tsx +24 -3
  163. package/src/core/styles.css +26 -23
  164. package/src/core/table/components/DataTable.tsx +167 -137
  165. package/src/core/table/components/DataTableFilters.tsx +1 -6
  166. package/src/core/table/components/DataTablePagination.tsx +51 -28
  167. package/src/core/table/components/DataTableToolbar.tsx +9 -4
  168. package/src/core/table/index.ts +1 -0
  169. package/src/core/table/interfaces/types.ts +13 -9
  170. package/src/demo/components/core/DemoDataTable.tsx +15 -19
  171. package/dist/admin/AdminApiKeys-C-6_Q-lH.js.map +0 -1
  172. package/dist/admin/AdminAudits-Bgbf04hO.js.map +0 -1
  173. package/dist/admin/AdminFiles-B9a7G3cY.js.map +0 -1
  174. package/dist/admin/AdminJobExecutions-B9cek5dl.js.map +0 -1
  175. package/dist/admin/AdminJobRegistry-DFgV3oqx.js.map +0 -1
  176. package/dist/admin/AdminLayout-DHsvWxVB.js +0 -70
  177. package/dist/admin/AdminLayout-DHsvWxVB.js.map +0 -1
  178. package/dist/admin/AdminSessions-BhGJPI3z.js.map +0 -1
  179. package/dist/admin/AdminUserSessions-1uzcx02z.js.map +0 -1
  180. package/dist/admin/AdminUsers-C85c3eiQ.js +0 -121
  181. package/dist/admin/AdminUsers-C85c3eiQ.js.map +0 -1
  182. package/dist/admin/auth-Dr0Cf8I7.js +0 -319
  183. package/dist/admin/auth-Dr0Cf8I7.js.map +0 -1
  184. package/dist/admin/core-2xoLiT0o.js.map +0 -1
  185. package/dist/auth/core-niW0sFLv.js.map +0 -1
  186. package/dist/demo/DemoDataTable-Z9xyV221.js.map +0 -1
  187. package/dist/demo/core-RCUw1Q-a.js.map +0 -1
@@ -1,4 +1,12 @@
1
- import { ActionButton, DataTable, Flex, Text } from "@alepha/ui";
1
+ import {
2
+ ActionButton,
3
+ DataTable,
4
+ Flex,
5
+ Text,
6
+ useDialog,
7
+ useToast,
8
+ } from "@alepha/ui";
9
+ import { Badge } from "@mantine/core";
2
10
  import {
3
11
  IconDeviceDesktop,
4
12
  IconDeviceMobile,
@@ -15,53 +23,61 @@ import { useClient } from "alepha/react";
15
23
  import { useI18n } from "alepha/react/i18n";
16
24
  import { useRouter } from "alepha/react/router";
17
25
  import { useState } from "react";
18
- import type { AdminRouter } from "../../AdminRouter.ts";
26
+ import type { AdminRouter } from "../../AdminRouter.tsx";
19
27
 
20
28
  export interface AdminSessionsProps {
21
29
  userRealmName?: string;
22
30
  }
23
31
 
32
+ const filters = t.object({
33
+ userId: t.optional(
34
+ t.uuid({
35
+ $control: {
36
+ query: t.pick(sessions.schema, ["userId"]),
37
+ },
38
+ }),
39
+ ),
40
+ });
41
+
42
+ const getDeviceIcon = (device?: string) => {
43
+ switch (device) {
44
+ case "MOBILE":
45
+ return <IconDeviceMobile size={14} />;
46
+ case "TABLET":
47
+ return <IconDeviceTablet size={14} />;
48
+ default:
49
+ return <IconDeviceDesktop size={14} />;
50
+ }
51
+ };
52
+
53
+ const isExpired = (expiresAt: Date | string) =>
54
+ new Date(expiresAt) < new Date();
55
+
24
56
  const AdminSessions = (props: AdminSessionsProps) => {
25
57
  const client = useClient<AdminSessionController>();
26
58
  const router = useRouter<AdminRouter>();
27
59
  const { l } = useI18n();
60
+ const dialog = useDialog();
61
+ const toast = useToast();
28
62
  const [refreshKey, setRefreshKey] = useState(0);
29
63
 
30
- const filters = t.object({
31
- userId: t.optional(
32
- t.uuid({
33
- $control: {
34
- query: t.pick(sessions.schema, ["userId"]),
35
- },
36
- }),
37
- ),
38
- });
39
-
40
- const getDeviceIcon = (device?: string) => {
41
- switch (device) {
42
- case "MOBILE":
43
- return <IconDeviceMobile size={14} />;
44
- case "TABLET":
45
- return <IconDeviceTablet size={14} />;
46
- default:
47
- return <IconDeviceDesktop size={14} />;
48
- }
49
- };
50
-
51
- const isExpired = (expiresAt: Date | string) => {
52
- return new Date(expiresAt) < new Date();
53
- };
54
-
55
- const handleDelete = async (sessionId: string) => {
64
+ const handleDelete = async (session: SessionEntity) => {
65
+ const confirmed = await dialog.confirm({
66
+ title: "Revoke session",
67
+ message:
68
+ "Are you sure you want to revoke this session? The user will be signed out.",
69
+ });
70
+ if (!confirmed) return;
56
71
  await client.deleteSession({
57
- params: { id: sessionId },
72
+ params: { id: session.id },
58
73
  query: { userRealmName: props.userRealmName },
59
74
  });
75
+ toast.success("Session revoked");
60
76
  setRefreshKey((k) => k + 1);
61
77
  };
62
78
 
63
79
  return (
64
- <Flex flex={1} direction="column">
80
+ <Flex p="md" flex={1} direction="column">
65
81
  <DataTable<SessionEntity, typeof filters>
66
82
  key={refreshKey}
67
83
  submitOnInit
@@ -74,20 +90,13 @@ const AdminSessions = (props: AdminSessionsProps) => {
74
90
  horizontalSpacing: "xs",
75
91
  verticalSpacing: "xs",
76
92
  }}
77
- onFilterChange={(key, _value, form) => {
78
- if (key === "userId") {
79
- return form.submit();
80
- }
81
- }}
93
+ onFilterChange={(_key, _value, form) => form.submit()}
82
94
  filters={filters}
83
- tableTrProps={(item) => {
84
- if (isExpired(item.expiresAt)) {
85
- return {
86
- opacity: 0.5,
87
- };
88
- }
89
- return {};
90
- }}
95
+ tableTrProps={(item) => ({
96
+ style: {
97
+ opacity: isExpired(item.expiresAt) ? 0.5 : 1,
98
+ },
99
+ })}
91
100
  items={async (filters) => {
92
101
  const response = await client.findSessions({
93
102
  query: {
@@ -95,7 +104,6 @@ const AdminSessions = (props: AdminSessionsProps) => {
95
104
  userRealmName: props.userRealmName,
96
105
  },
97
106
  });
98
-
99
107
  return response as Page<SessionEntity>;
100
108
  }}
101
109
  columns={{
@@ -117,7 +125,6 @@ const AdminSessions = (props: AdminSessionsProps) => {
117
125
  },
118
126
  userAgent: {
119
127
  label: "Device",
120
- fit: true,
121
128
  value: (item) => (
122
129
  <Flex gap={4} align="center">
123
130
  {item.userAgent ? (
@@ -128,8 +135,8 @@ const AdminSessions = (props: AdminSessionsProps) => {
128
135
  </Text>
129
136
  </>
130
137
  ) : (
131
- <Text size="xs" c="dimmed">
132
- -
138
+ <Text size="xs" muted>
139
+
133
140
  </Text>
134
141
  )}
135
142
  </Flex>
@@ -137,49 +144,42 @@ const AdminSessions = (props: AdminSessionsProps) => {
137
144
  },
138
145
  ip: {
139
146
  label: "IP",
140
- fit: true,
141
147
  value: (item) => (
142
- <Text size="xs" ff="monospace" c="dimmed">
143
- {item.ip || "-"}
148
+ <Text size="xs" ff="monospace" muted>
149
+ {item.ip || ""}
144
150
  </Text>
145
151
  ),
146
152
  },
147
153
  expiresAt: {
148
154
  label: "Status",
149
- fit: true,
150
155
  value: (item) => (
151
- <Text
152
- size="xs"
153
- c={isExpired(item.expiresAt) ? "dimmed" : undefined}
156
+ <Badge
157
+ size="sm"
158
+ variant="light"
159
+ color={isExpired(item.expiresAt) ? "gray" : "green"}
154
160
  >
155
161
  {isExpired(item.expiresAt) ? "Expired" : "Active"}
156
- </Text>
162
+ </Badge>
157
163
  ),
158
164
  },
159
165
  createdAt: {
160
166
  label: "Created",
161
- fit: true,
162
167
  value: (item) => (
163
- <Text size="xs" c="dimmed">
168
+ <Text size="xs" muted>
164
169
  {l(item.createdAt, { date: "fromNow" })}
165
170
  </Text>
166
171
  ),
167
172
  },
168
- actions: {
169
- label: "",
170
- fit: true,
171
- value: (item) => (
172
- <ActionButton
173
- size="xs"
174
- variant="subtle"
175
- color="red"
176
- onClick={() => handleDelete(item.id)}
177
- >
178
- <IconTrash size={14} />
179
- </ActionButton>
180
- ),
181
- },
182
173
  }}
174
+ rowActions={(item) => [
175
+ {
176
+ label: "Revoke session",
177
+ icon: IconTrash,
178
+ color: "red",
179
+ onClick: () => handleDelete(item),
180
+ visible: !isExpired(item.expiresAt),
181
+ },
182
+ ]}
183
183
  />
184
184
  </Flex>
185
185
  );
@@ -1,4 +1,5 @@
1
- import { DataTable, Flex, Text, useDialog } from "@alepha/ui";
1
+ import { DataTable, Flex, Text, useDialog, useToast } from "@alepha/ui";
2
+ import { Badge } from "@mantine/core";
2
3
  import {
3
4
  IconDeviceDesktop,
4
5
  IconDeviceMobile,
@@ -38,20 +39,22 @@ const AdminUserSessions = (props: AdminUserSessionsProps) => {
38
39
  const client = useClient<AdminSessionController>();
39
40
  const { l } = useI18n();
40
41
  const dialog = useDialog();
42
+ const toast = useToast();
41
43
  const [refreshKey, setRefreshKey] = useState(0);
42
44
 
43
- const handleDeleteSession = async (sessionId: string) => {
45
+ const handleDeleteSession = async (session: SessionEntity) => {
44
46
  const confirmed = await dialog.confirm({
45
- title: "Revoke Session",
46
- message: "Are you sure you want to revoke this session?",
47
+ title: "Revoke session",
48
+ message:
49
+ "Are you sure you want to revoke this session? The user will be signed out on this device.",
47
50
  });
48
- if (confirmed) {
49
- await client.deleteSession({
50
- params: { id: sessionId },
51
- query: { userRealmName: props.userRealmName },
52
- });
53
- setRefreshKey((k) => k + 1);
54
- }
51
+ if (!confirmed) return;
52
+ await client.deleteSession({
53
+ params: { id: session.id },
54
+ query: { userRealmName: props.userRealmName },
55
+ });
56
+ toast.success("Session revoked");
57
+ setRefreshKey((k) => k + 1);
55
58
  };
56
59
 
57
60
  return (
@@ -88,50 +91,49 @@ const AdminUserSessions = (props: AdminUserSessionsProps) => {
88
91
  <Text size="xs">
89
92
  {item.userAgent
90
93
  ? `${item.userAgent.browser} / ${item.userAgent.os}`
91
- : "\u2014"}
94
+ : ""}
92
95
  </Text>
93
96
  </Flex>
94
97
  ),
95
98
  },
96
99
  ip: {
97
100
  label: "IP",
98
- fit: true,
99
101
  value: (item) => (
100
- <Text size="xs" ff="monospace" c="dimmed">
101
- {item.ip || "\u2014"}
102
+ <Text size="xs" ff="monospace" muted>
103
+ {item.ip || ""}
102
104
  </Text>
103
105
  ),
104
106
  },
105
107
  status: {
106
108
  label: "Status",
107
- fit: true,
108
109
  value: (item) => (
109
- <Text size="xs" c="dimmed">
110
+ <Badge
111
+ size="sm"
112
+ variant="light"
113
+ color={isExpired(item.expiresAt) ? "gray" : "green"}
114
+ >
110
115
  {isExpired(item.expiresAt) ? "Expired" : "Active"}
111
- </Text>
116
+ </Badge>
112
117
  ),
113
118
  },
114
119
  createdAt: {
115
120
  label: "Created",
116
- fit: true,
117
121
  value: (item) => (
118
- <Text size="xs" c="dimmed">
122
+ <Text size="xs" muted>
119
123
  {l(item.createdAt, { date: "fromNow" })}
120
124
  </Text>
121
125
  ),
122
126
  },
123
- actions: {
124
- label: "",
125
- fit: true,
126
- actions: (item) => [
127
- {
128
- icon: <IconTrash size={14} />,
129
- onClick: () => handleDeleteSession(item.id),
130
- tooltip: "Revoke session",
131
- },
132
- ],
133
- },
134
127
  }}
128
+ rowActions={(item) => [
129
+ {
130
+ label: "Revoke session",
131
+ icon: IconTrash,
132
+ color: "red",
133
+ onClick: () => handleDeleteSession(item),
134
+ visible: !isExpired(item.expiresAt),
135
+ },
136
+ ]}
135
137
  />
136
138
  );
137
139
  };
@@ -1,32 +1,34 @@
1
- import { DataTable, Flex, Text, useDialog, useToast } from "@alepha/ui";
2
- import { Badge } from "@mantine/core";
1
+ import {
2
+ ActionButton,
3
+ DataTable,
4
+ Flex,
5
+ Text,
6
+ useDialog,
7
+ useToast,
8
+ } from "@alepha/ui";
9
+ import { Avatar, Badge } from "@mantine/core";
10
+ import {
11
+ IconEye,
12
+ IconTrash,
13
+ IconUserOff,
14
+ IconUserPlus,
15
+ } from "@tabler/icons-react";
3
16
  import { type Page, t } from "alepha";
4
17
  import type { AdminUserController, UserEntity } from "alepha/api/users";
5
18
  import { useClient } from "alepha/react";
6
19
  import { useI18n } from "alepha/react/i18n";
7
20
  import { useRouter } from "alepha/react/router";
8
21
  import { useState } from "react";
9
- import type { AdminRouter } from "../../AdminRouter.ts";
22
+ import type { AdminRouter } from "../../AdminRouter.tsx";
10
23
 
11
24
  export interface AdminUsersProps {
12
25
  userRealmName?: string;
13
26
  }
14
27
 
15
- const createUserSchema = t.object({
16
- username: t.optional(
17
- t.shortText({
18
- minLength: 3,
19
- maxLength: 50,
20
- pattern: "^[a-zA-Z0-9._-]+$",
21
- }),
22
- ),
23
- email: t.optional(t.email()),
24
- phoneNumber: t.optional(t.e164()),
25
- firstName: t.optional(t.string()),
26
- lastName: t.optional(t.string()),
27
- roles: t.optional(t.array(t.string())),
28
+ const filters = t.object({
29
+ query: t.optional(t.text()),
28
30
  enabled: t.optional(t.boolean()),
29
- password: t.optional(t.string({ minLength: 8 })),
31
+ emailVerified: t.optional(t.boolean()),
30
32
  });
31
33
 
32
34
  const AdminUsers = (props: AdminUsersProps) => {
@@ -37,52 +39,94 @@ const AdminUsers = (props: AdminUsersProps) => {
37
39
  const toast = useToast();
38
40
  const [refreshKey, setRefreshKey] = useState(0);
39
41
 
40
- const filters = t.object({
41
- query: t.optional(
42
- t.string({
43
- $control: {
44
- query: t.object({
45
- email: t.optional(t.email()),
46
- enabled: t.optional(t.boolean()),
47
- emailVerified: t.optional(t.boolean()),
48
- }),
49
- },
50
- }),
51
- ),
52
- });
42
+ const refresh = () => setRefreshKey((k) => k + 1);
43
+
44
+ const handleToggleEnabled = async (user: UserEntity) => {
45
+ const enabled = !user.enabled;
46
+ const confirmed = await dialog.confirm({
47
+ title: enabled ? "Enable user" : "Disable user",
48
+ message: enabled
49
+ ? `Enable ${user.email || user.username || "this user"}?`
50
+ : `Disable ${user.email || user.username || "this user"}? They will no longer be able to sign in.`,
51
+ });
52
+ if (!confirmed) return;
53
+ await client.updateUser({
54
+ params: { id: user.id },
55
+ query: { userRealmName: props.userRealmName },
56
+ body: { enabled },
57
+ });
58
+ toast.success({
59
+ message: enabled ? "User enabled" : "User disabled",
60
+ });
61
+ refresh();
62
+ };
63
+
64
+ const handleDelete = async (user: UserEntity) => {
65
+ const confirmed = await dialog.confirm({
66
+ title: "Delete user",
67
+ message: `Permanently delete ${user.email || user.username || "this user"}? This action cannot be undone.`,
68
+ });
69
+ if (!confirmed) return;
70
+ await client.deleteUser({
71
+ params: { id: user.id },
72
+ query: { userRealmName: props.userRealmName },
73
+ });
74
+ toast.success({ message: "User deleted" });
75
+ refresh();
76
+ };
77
+
78
+ const handleBulkDisable = async (
79
+ items: UserEntity[],
80
+ clearSelection: () => void,
81
+ ) => {
82
+ const enabledUsers = items.filter((u) => u.enabled);
83
+ if (enabledUsers.length === 0) {
84
+ toast.danger({ message: "No active users in selection" });
85
+ return;
86
+ }
87
+ const confirmed = await dialog.confirm({
88
+ title: "Disable users",
89
+ message: `Disable ${enabledUsers.length} user(s)? They will no longer be able to sign in.`,
90
+ });
91
+ if (!confirmed) return;
92
+ for (const user of enabledUsers) {
93
+ await client.updateUser({
94
+ params: { id: user.id },
95
+ query: { userRealmName: props.userRealmName },
96
+ body: { enabled: false },
97
+ });
98
+ }
99
+ toast.success({
100
+ message: `${enabledUsers.length} user(s) disabled`,
101
+ });
102
+ clearSelection();
103
+ refresh();
104
+ };
53
105
 
54
106
  return (
55
- <Flex flex={1} direction="column">
107
+ <Flex p={"md"} flex={1} direction="column">
56
108
  <DataTable<UserEntity, typeof filters>
109
+ withCheckbox
110
+ withExport
111
+ checkboxActions={[
112
+ {
113
+ intent: "danger",
114
+ label: "Disable selected",
115
+ icon: <IconUserOff />,
116
+ onClick: (ctx) =>
117
+ handleBulkDisable(ctx.selectedItems, ctx.clearSelection),
118
+ },
119
+ ]}
57
120
  key={refreshKey}
58
121
  submitOnInit
59
122
  defaultSize={10}
60
- typeFormProps={{
61
- skipSubmitButton: true,
62
- columns: 3,
63
- }}
123
+ defaultFilters={["query"]}
64
124
  tableProps={{
65
125
  horizontalSpacing: "xs",
66
126
  verticalSpacing: "xs",
67
- striped: false,
68
- highlightOnHover: true,
69
- }}
70
- onFilterChange={(key, _value, form) => {
71
- if (key === "query") {
72
- return form.submit();
73
- }
74
127
  }}
128
+ onFilterChange={(_key, _value, form) => form.submit()}
75
129
  filters={filters}
76
- tableTrProps={(item) => ({
77
- style: {
78
- cursor: "pointer",
79
- opacity: item.enabled ? 1 : 0.5,
80
- },
81
- onClick: () =>
82
- router.push("adminUserProfile", {
83
- params: { userId: item.id },
84
- }),
85
- })}
86
130
  items={async (filters) => {
87
131
  const response = await client.findUsers({
88
132
  query: {
@@ -93,49 +137,117 @@ const AdminUsers = (props: AdminUsersProps) => {
93
137
  return response as Page<UserEntity>;
94
138
  }}
95
139
  columns={{
140
+ user: {
141
+ label: "User",
142
+ value: (item) => {
143
+ const name =
144
+ `${item.firstName || ""} ${item.lastName || ""}`.trim() ||
145
+ item.username ||
146
+ "Anonymous";
147
+
148
+ return (
149
+ <ActionButton
150
+ variant={"transparent"}
151
+ href={`/admin/users/${item.id}`}
152
+ >
153
+ <Flex gap={"xs"} centerY>
154
+ <Avatar size={"sm"} name={name} />
155
+ <Flex col>
156
+ <Text size={"sm"} bold>
157
+ {name}
158
+ </Text>
159
+ <Text size="xs" muted>
160
+ {item.email || "\u2014"}
161
+ </Text>
162
+ </Flex>
163
+ </Flex>
164
+ </ActionButton>
165
+ );
166
+ },
167
+ },
96
168
  username: {
97
169
  label: "Username",
170
+ defaultHidden: true,
98
171
  value: (item) => (
99
- <Text size="sm" fw={500}>
172
+ <Text size="sm" ff="monospace">
100
173
  {item.username || "\u2014"}
101
174
  </Text>
102
175
  ),
103
176
  },
104
- email: {
105
- label: "Email",
106
- value: (item) => <Text size="sm">{item.email || "\u2014"}</Text>,
107
- },
108
177
  roles: {
109
178
  label: "Roles",
110
- value: (item) => (
111
- <Flex gap={4}>
112
- {item.roles.map((role: string) => (
113
- <Badge key={role} size="xs" variant="default">
114
- {role}
115
- </Badge>
116
- ))}
117
- </Flex>
118
- ),
179
+ value: (item) =>
180
+ item.roles.length > 0 ? (
181
+ <Flex gap={4}>
182
+ {item.roles.map((role: string) => (
183
+ <Badge key={role} size="xs" variant="default">
184
+ {role}
185
+ </Badge>
186
+ ))}
187
+ </Flex>
188
+ ) : (
189
+ <Text size="xs" muted>
190
+ No roles
191
+ </Text>
192
+ ),
119
193
  },
120
194
  enabled: {
121
195
  label: "Status",
122
- fit: true,
123
196
  value: (item) => (
124
- <Text size="sm" c="dimmed">
197
+ <Badge
198
+ size="sm"
199
+ variant="light"
200
+ color={item.enabled ? "green" : "red"}
201
+ >
125
202
  {item.enabled ? "Active" : "Disabled"}
126
- </Text>
203
+ </Badge>
204
+ ),
205
+ },
206
+ emailVerified: {
207
+ label: "Email",
208
+ value: (item) => (
209
+ <Badge
210
+ size="sm"
211
+ variant="light"
212
+ color={item.emailVerified ? "green" : "gray"}
213
+ >
214
+ {item.emailVerified ? "Verified" : "Unverified"}
215
+ </Badge>
127
216
  ),
128
217
  },
129
218
  createdAt: {
130
- label: "Created",
131
- fit: true,
219
+ label: "Joined",
220
+ sortable: true,
221
+ sortKey: "createdAt",
132
222
  value: (item) => (
133
- <Text size="xs" c="dimmed">
223
+ <Text size="xs" muted>
134
224
  {l(item.createdAt, { date: "fromNow" })}
135
225
  </Text>
136
226
  ),
137
227
  },
138
228
  }}
229
+ rowActions={(item) => [
230
+ {
231
+ label: "View profile",
232
+ icon: IconEye,
233
+ onClick: () =>
234
+ router.push("adminUserProfile", {
235
+ params: { userId: item.id },
236
+ }),
237
+ },
238
+ {
239
+ label: item.enabled ? "Disable user" : "Enable user",
240
+ icon: item.enabled ? IconUserOff : IconUserPlus,
241
+ color: item.enabled ? "red" : "green",
242
+ onClick: () => handleToggleEnabled(item),
243
+ },
244
+ {
245
+ label: "Delete user",
246
+ icon: IconTrash,
247
+ color: "red",
248
+ onClick: () => handleDelete(item),
249
+ },
250
+ ]}
139
251
  />
140
252
  </Flex>
141
253
  );
@@ -1,11 +1,11 @@
1
1
  import { AlephaUI } from "@alepha/ui";
2
2
  import { AlephaUIAuth } from "@alepha/ui/auth";
3
3
  import { $module } from "alepha";
4
- import { AdminRouter } from "./AdminRouter.ts";
4
+ import { AdminRouter } from "./AdminRouter.tsx";
5
5
 
6
6
  // ---------------------------------------------------------------------------------------------------------------------
7
7
 
8
- export { AdminRouter } from "./AdminRouter.ts";
8
+ export { AdminRouter } from "./AdminRouter.tsx";
9
9
  export * from "./primitives/$uiAdmin.ts";
10
10
 
11
11
  // ---------------------------------------------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  import type { DashboardShellProps } from "@alepha/ui";
2
2
  import { $context } from "alepha";
3
- import { AdminRouter } from "../AdminRouter.ts";
3
+ import { AdminRouter } from "../AdminRouter.tsx";
4
4
 
5
5
  /**
6
6
  * Register Admin UI components and get the AdminRouter instance.