@alepha/ui 0.14.2 → 0.14.4

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 (54) hide show
  1. package/dist/admin/AdminAudits-DIrCCPk3.js.map +1 -1
  2. package/dist/admin/AdminNotifications-cIbywWKi.js.map +1 -1
  3. package/dist/admin/AdminParameters-D-q3Qmhv.js.map +1 -1
  4. package/dist/admin/AdminSessions-vOgkrQ2U.js.map +1 -1
  5. package/dist/admin/AdminUserAudits-CSsN1fIC.js.map +1 -1
  6. package/dist/admin/AdminUserCreate-B72nu-3W.js.map +1 -1
  7. package/dist/admin/AdminUserDetails-CKM2IEMr.js +475 -0
  8. package/dist/admin/AdminUserDetails-CKM2IEMr.js.map +1 -0
  9. package/dist/admin/{AdminUserDetails-z1y8kJeB.js → AdminUserDetails-Zib_B6Al.js} +1 -1
  10. package/dist/admin/{AdminUserLayout-DyQYacQQ.js → AdminUserLayout-BNBOEiAO.js} +1 -1
  11. package/dist/admin/AdminUserLayout-D7En9UBq.js +334 -0
  12. package/dist/admin/AdminUserLayout-D7En9UBq.js.map +1 -0
  13. package/dist/admin/AdminUserSessions-DEaGu6n6.js.map +1 -1
  14. package/dist/admin/{AdminUserSettings-CR7MxX_R.js → AdminUserSettings-Di73D7g2.js} +6 -5
  15. package/dist/admin/AdminUserSettings-Di73D7g2.js.map +1 -0
  16. package/dist/admin/AdminUserSettings-yI-JECf5.js +3 -0
  17. package/dist/admin/AdminUsers-BnGIRvmV.js.map +1 -1
  18. package/dist/admin/index.d.ts +10 -10
  19. package/dist/admin/index.d.ts.map +1 -1
  20. package/dist/admin/index.js +18 -18
  21. package/dist/admin/index.js.map +1 -1
  22. package/dist/auth/index.js +4 -4
  23. package/dist/auth/index.js.map +1 -1
  24. package/dist/core/index.d.ts.map +1 -1
  25. package/dist/core/index.js +6 -5
  26. package/dist/core/index.js.map +1 -1
  27. package/package.json +11 -11
  28. package/src/admin/AdminRouter.ts +23 -20
  29. package/src/admin/MainRouter.ts +1 -1
  30. package/src/admin/components/audits/AdminAudits.tsx +2 -2
  31. package/src/admin/components/jobs/AdminJobs.tsx +2 -2
  32. package/src/admin/components/notifications/AdminNotifications.tsx +2 -2
  33. package/src/admin/components/parameters/AdminParameters.tsx +2 -2
  34. package/src/admin/components/sessions/AdminSessions.tsx +2 -2
  35. package/src/admin/components/shared/AdminResourceHeader.tsx +281 -0
  36. package/src/admin/components/shared/AdminResourceTabs.tsx +94 -0
  37. package/src/admin/components/shared/index.ts +10 -0
  38. package/src/admin/components/users/AdminUserAudits.tsx +2 -2
  39. package/src/admin/components/users/AdminUserCreate.tsx +2 -2
  40. package/src/admin/components/users/AdminUserDetails.tsx +337 -85
  41. package/src/admin/components/users/AdminUserLayout.tsx +164 -108
  42. package/src/admin/components/users/AdminUserSessions.tsx +2 -2
  43. package/src/admin/components/users/AdminUserSettings.tsx +10 -5
  44. package/src/admin/components/users/AdminUsers.tsx +6 -2
  45. package/src/auth/AuthRouter.ts +4 -4
  46. package/src/core/components/form/TypeForm.tsx +3 -2
  47. package/src/core/components/layout/AlephaMantineProvider.tsx +5 -1
  48. package/src/core/components/layout/Sidebar.tsx +9 -6
  49. package/dist/admin/AdminUserDetails-BCt8Su-4.js +0 -222
  50. package/dist/admin/AdminUserDetails-BCt8Su-4.js.map +0 -1
  51. package/dist/admin/AdminUserLayout-Ck0GLRE5.js +0 -151
  52. package/dist/admin/AdminUserLayout-Ck0GLRE5.js.map +0 -1
  53. package/dist/admin/AdminUserSettings-CE66UTIP.js +0 -3
  54. package/dist/admin/AdminUserSettings-CR7MxX_R.js.map +0 -1
@@ -1,11 +1,23 @@
1
1
  import { useClient } from "@alepha/react";
2
2
  import { NestedView, useRouter, useRouterState } from "@alepha/react/router";
3
- import { ActionButton, Flex, Text } from "@alepha/ui";
4
- import { Avatar, Badge, Card, Group, Loader, Stack, Tabs } from "@mantine/core";
5
- import { IconDevices, IconSettings, IconUser } from "@tabler/icons-react";
6
- import type { UserController, UserEntity } from "alepha/api/users";
3
+ import { Box, Center, Loader, Stack, Text } from "@mantine/core";
4
+ import {
5
+ IconBan,
6
+ IconDevices,
7
+ IconHistory,
8
+ IconLock,
9
+ IconMail,
10
+ IconPencil,
11
+ IconSettings,
12
+ IconShieldCheck,
13
+ IconTrash,
14
+ IconUser,
15
+ } from "@tabler/icons-react";
16
+ import type { AdminUserController, UserEntity } from "alepha/api/users";
7
17
  import { useEffect, useState } from "react";
8
18
  import type { AdminRouter } from "../../AdminRouter.ts";
19
+ import AdminResourceHeader from "../shared/AdminResourceHeader.tsx";
20
+ import AdminResourceTabs from "../shared/AdminResourceTabs.tsx";
9
21
 
10
22
  export interface AdminUserLayoutProps {
11
23
  userRealmName?: string;
@@ -14,11 +26,12 @@ export interface AdminUserLayoutProps {
14
26
  const AdminUserLayout = (props: AdminUserLayoutProps) => {
15
27
  const router = useRouter<AdminRouter>();
16
28
  const state = useRouterState();
17
- const client = useClient<UserController>();
29
+ const client = useClient<AdminUserController>();
18
30
  const userId = state.params.userId as string;
19
31
 
20
32
  const [user, setUser] = useState<UserEntity | null>(null);
21
33
  const [loading, setLoading] = useState(true);
34
+ const [actionLoading, setActionLoading] = useState<string | null>(null);
22
35
 
23
36
  useEffect(() => {
24
37
  const loadUser = async () => {
@@ -38,127 +51,170 @@ const AdminUserLayout = (props: AdminUserLayoutProps) => {
38
51
 
39
52
  if (loading) {
40
53
  return (
41
- <Flex flex={1} justify="center" align="center">
54
+ <Center flex={1}>
42
55
  <Loader />
43
- </Flex>
56
+ </Center>
44
57
  );
45
58
  }
46
59
 
47
60
  if (!user) {
48
61
  return (
49
- <Flex flex={1} justify="center" align="center">
50
- <Text c="dimmed">User not found</Text>
51
- </Flex>
62
+ <Center flex={1}>
63
+ <Stack align="center" gap="xs">
64
+ <IconUser size={48} opacity={0.3} />
65
+ <Text c="dimmed">User not found</Text>
66
+ </Stack>
67
+ </Center>
52
68
  );
53
69
  }
54
70
 
55
- const currentPath = state.url.pathname;
56
- const detailsPath = router.path("adminUserDetails", { params: { userId } });
57
- const sessionsPath = router.path("adminUserSessions", { params: { userId } });
58
- const settingsPath = router.path("adminUserSettings", { params: { userId } });
71
+ const displayName =
72
+ user.firstName || user.lastName
73
+ ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim()
74
+ : user.username || user.email || "User";
59
75
 
76
+ const currentPath = state.url.pathname;
60
77
  const getActiveTab = () => {
61
78
  if (currentPath.endsWith("/sessions")) return "sessions";
62
79
  if (currentPath.endsWith("/settings")) return "settings";
63
- return "details";
80
+ if (currentPath.endsWith("/audits")) return "audits";
81
+ return "profile";
64
82
  };
65
- const activeTab = getActiveTab();
66
83
 
67
- const displayName =
68
- user.firstName || user.lastName
69
- ? `${user.firstName ?? ""} ${user.lastName ?? ""}`.trim()
70
- : user.username || user.email || "User";
84
+ const handleBlockUser = async () => {
85
+ setActionLoading("block");
86
+ try {
87
+ const updated = await client.updateUser({
88
+ params: { id: userId },
89
+ query: { userRealmName: props.userRealmName },
90
+ body: { enabled: !user.enabled },
91
+ });
92
+ setUser(updated);
93
+ } finally {
94
+ setActionLoading(null);
95
+ }
96
+ };
97
+
98
+ const handleSendVerification = async () => {
99
+ setActionLoading("verify");
100
+ // TODO: Implement send verification
101
+ await new Promise((resolve) => setTimeout(resolve, 1000));
102
+ setActionLoading(null);
103
+ };
104
+
105
+ const handleResetPassword = async () => {
106
+ setActionLoading("reset");
107
+ // TODO: Implement reset password
108
+ await new Promise((resolve) => setTimeout(resolve, 1000));
109
+ setActionLoading(null);
110
+ };
111
+
112
+ const handleDeleteUser = async () => {
113
+ if (
114
+ !confirm(
115
+ "Are you sure you want to delete this user? This action cannot be undone.",
116
+ )
117
+ ) {
118
+ return;
119
+ }
120
+ setActionLoading("delete");
121
+ try {
122
+ await client.deleteUser({
123
+ params: { id: userId },
124
+ query: { userRealmName: props.userRealmName },
125
+ });
126
+ await router.go("adminUsers");
127
+ } finally {
128
+ setActionLoading(null);
129
+ }
130
+ };
71
131
 
72
132
  return (
73
- <Flex flex={1} direction="column" gap="md" p="md">
74
- <Card withBorder p="md">
75
- <Group>
76
- <Avatar size="lg" radius="xl" color="blue">
77
- {displayName.charAt(0).toUpperCase()}
78
- </Avatar>
79
- <Stack gap={4}>
80
- <Group gap="xs">
81
- <Text size="lg" fw={500}>
82
- {displayName}
83
- </Text>
84
- <Badge
85
- size="sm"
86
- variant="light"
87
- color={user.enabled ? "green" : "red"}
88
- >
89
- {user.enabled ? "Active" : "Disabled"}
90
- </Badge>
91
- </Group>
92
- <Text size="sm" c="dimmed">
93
- {user.email || user.username || user.id}
94
- </Text>
95
- {user.roles.length > 0 && (
96
- <Group gap={4}>
97
- {user.roles.map((role: string) => (
98
- <Badge key={role} size="xs" variant="outline">
99
- {role}
100
- </Badge>
101
- ))}
102
- </Group>
103
- )}
104
- </Stack>
105
- </Group>
106
- </Card>
107
-
108
- <Tabs value={activeTab}>
109
- <Tabs.List>
110
- <ActionButton
111
- href={detailsPath}
112
- leftSection={<IconUser size={16} />}
113
- c={activeTab === "details" ? undefined : "dimmed"}
114
- fw={activeTab === "details" ? 500 : 400}
115
- style={{
116
- borderBottom:
117
- activeTab === "details"
118
- ? "2px solid var(--mantine-primary-color-filled)"
119
- : "2px solid transparent",
120
- borderRadius: 0,
121
- }}
122
- >
123
- Details
124
- </ActionButton>
125
- <ActionButton
126
- href={sessionsPath}
127
- leftSection={<IconDevices size={16} />}
128
- c={activeTab === "sessions" ? undefined : "dimmed"}
129
- fw={activeTab === "sessions" ? 500 : 400}
130
- style={{
131
- borderBottom:
132
- activeTab === "sessions"
133
- ? "2px solid var(--mantine-primary-color-filled)"
134
- : "2px solid transparent",
135
- borderRadius: 0,
136
- }}
137
- >
138
- Sessions
139
- </ActionButton>
140
- <ActionButton
141
- href={settingsPath}
142
- leftSection={<IconSettings size={16} />}
143
- c={activeTab === "settings" ? undefined : "dimmed"}
144
- fw={activeTab === "settings" ? 500 : 400}
145
- style={{
146
- borderBottom:
147
- activeTab === "settings"
148
- ? "2px solid var(--mantine-primary-color-filled)"
149
- : "2px solid transparent",
150
- borderRadius: 0,
151
- }}
152
- >
153
- Settings
154
- </ActionButton>
155
- </Tabs.List>
156
- </Tabs>
157
-
158
- <Flex flex={1}>
133
+ <Box py="xl" px="xl" flex={1}>
134
+ <Stack gap="lg">
135
+ <AdminResourceHeader
136
+ backHref={router.path("adminUsers")}
137
+ backLabel="Users"
138
+ avatar={user.picture || displayName.charAt(0).toUpperCase()}
139
+ avatarColor={user.enabled ? "blue" : "gray"}
140
+ title={displayName}
141
+ subtitle={user.email || user.username || undefined}
142
+ status={{
143
+ label: user.enabled ? "Active" : "Disabled",
144
+ color: user.enabled ? "green" : "red",
145
+ }}
146
+ menuActions={[
147
+ {
148
+ label: "Edit Profile",
149
+ icon: IconPencil,
150
+ href: router.path("adminUserDetails", { params: { userId } }),
151
+ },
152
+ {
153
+ label: user.enabled ? "Disable User" : "Enable User",
154
+ icon: user.enabled ? IconBan : IconShieldCheck,
155
+ color: user.enabled ? "orange" : "green",
156
+ onClick: handleBlockUser,
157
+ loading: actionLoading === "block",
158
+ },
159
+ ...(user.email && !user.emailVerified
160
+ ? [
161
+ {
162
+ label: "Send Verification Email",
163
+ icon: IconMail,
164
+ onClick: handleSendVerification,
165
+ loading: actionLoading === "verify",
166
+ },
167
+ ]
168
+ : []),
169
+ {
170
+ label: "Reset Password",
171
+ icon: IconLock,
172
+ onClick: handleResetPassword,
173
+ loading: actionLoading === "reset",
174
+ },
175
+ {
176
+ label: "Delete User",
177
+ icon: IconTrash,
178
+ color: "red",
179
+ onClick: handleDeleteUser,
180
+ loading: actionLoading === "delete",
181
+ },
182
+ ]}
183
+ />
184
+
185
+ <AdminResourceTabs
186
+ activeTab={getActiveTab()}
187
+ tabs={[
188
+ {
189
+ value: "profile",
190
+ label: "Profile",
191
+ icon: IconUser,
192
+ href: router.path("adminUserDetails", { params: { userId } }),
193
+ },
194
+ {
195
+ value: "sessions",
196
+ label: "Sessions",
197
+ icon: IconDevices,
198
+ href: router.path("adminUserSessions", { params: { userId } }),
199
+ },
200
+ {
201
+ value: "audits",
202
+ label: "Activity",
203
+ icon: IconHistory,
204
+ href: router.path("adminUserAudits", { params: { userId } }),
205
+ },
206
+ {
207
+ value: "settings",
208
+ label: "Settings",
209
+ icon: IconSettings,
210
+ href: router.path("adminUserSettings", { params: { userId } }),
211
+ },
212
+ ]}
213
+ />
214
+
159
215
  <NestedView />
160
- </Flex>
161
- </Flex>
216
+ </Stack>
217
+ </Box>
162
218
  );
163
219
  };
164
220
 
@@ -10,7 +10,7 @@ import {
10
10
  IconTrash,
11
11
  } from "@tabler/icons-react";
12
12
  import { type Page, t } from "alepha";
13
- import type { SessionController, SessionEntity } from "alepha/api/users";
13
+ import type { AdminSessionController, SessionEntity } from "alepha/api/users";
14
14
  import { useState } from "react";
15
15
 
16
16
  export interface AdminUserSessionsProps {
@@ -19,7 +19,7 @@ export interface AdminUserSessionsProps {
19
19
 
20
20
  const AdminUserSessions = (props: AdminUserSessionsProps) => {
21
21
  const state = useRouterState();
22
- const client = useClient<SessionController>();
22
+ const client = useClient<AdminSessionController>();
23
23
  const { l } = useI18n();
24
24
  const userId = state.params.userId as string;
25
25
  const [refreshKey, setRefreshKey] = useState(0);
@@ -8,7 +8,11 @@ import {
8
8
  IconMail,
9
9
  IconTrash,
10
10
  } from "@tabler/icons-react";
11
- import type { UserController, UserEntity } from "alepha/api/users";
11
+ import type {
12
+ AdminUserController,
13
+ UserController,
14
+ UserEntity,
15
+ } from "alepha/api/users";
12
16
  import { useEffect, useState } from "react";
13
17
  import type { AdminRouter } from "../../AdminRouter.ts";
14
18
 
@@ -19,7 +23,8 @@ export interface AdminUserSettingsProps {
19
23
  const AdminUserSettings = (props: AdminUserSettingsProps) => {
20
24
  const router = useRouter<AdminRouter>();
21
25
  const state = useRouterState();
22
- const client = useClient<UserController>();
26
+ const adminClient = useClient<AdminUserController>();
27
+ const userClient = useClient<UserController>();
23
28
  const userId = state.params.userId as string;
24
29
 
25
30
  const [user, setUser] = useState<UserEntity | null>(null);
@@ -31,7 +36,7 @@ const AdminUserSettings = (props: AdminUserSettingsProps) => {
31
36
  useEffect(() => {
32
37
  const loadUser = async () => {
33
38
  try {
34
- const data = await client.getUser({
39
+ const data = await adminClient.getUser({
35
40
  params: { id: userId },
36
41
  query: { userRealmName: props.userRealmName },
37
42
  });
@@ -51,7 +56,7 @@ const AdminUserSettings = (props: AdminUserSettingsProps) => {
51
56
 
52
57
  setDeleteLoading(true);
53
58
  try {
54
- await client.deleteUser({
59
+ await adminClient.deleteUser({
55
60
  params: { id: userId },
56
61
  query: { userRealmName: props.userRealmName },
57
62
  });
@@ -67,7 +72,7 @@ const AdminUserSettings = (props: AdminUserSettingsProps) => {
67
72
  setVerifyLoading(true);
68
73
  setVerifySuccess(false);
69
74
  try {
70
- await client.requestEmailVerification({
75
+ await userClient.requestEmailVerification({
71
76
  query: {
72
77
  userRealmName: props.userRealmName,
73
78
  method: "link",
@@ -5,7 +5,11 @@ import { DataTable, Text } from "@alepha/ui";
5
5
  import { Badge, Flex, Group } from "@mantine/core";
6
6
  import { IconCheck, IconUsersPlus, IconX } from "@tabler/icons-react";
7
7
  import { type Page, t } from "alepha";
8
- import { type UserController, type UserEntity, users } from "alepha/api/users";
8
+ import {
9
+ type AdminUserController,
10
+ type UserEntity,
11
+ users,
12
+ } from "alepha/api/users";
9
13
  import type { AdminRouter } from "../../AdminRouter.ts";
10
14
 
11
15
  export interface AdminUsersProps {
@@ -13,7 +17,7 @@ export interface AdminUsersProps {
13
17
  }
14
18
 
15
19
  const AdminUsers = (props: AdminUsersProps) => {
16
- const client = useClient<UserController>();
20
+ const client = useClient<AdminUserController>();
17
21
  const router = useRouter<AdminRouter>();
18
22
  const { l } = useI18n();
19
23
 
@@ -47,7 +47,7 @@ export class AuthRouter {
47
47
  },
48
48
  can: () => !this.auth.user,
49
49
  lazy: () => import("./components/Login.tsx"),
50
- resolve: async ({ query }) => {
50
+ loader: async ({ query }) => {
51
51
  return {
52
52
  realmConfig: await this.loadRealmConfig(query.realm),
53
53
  };
@@ -64,7 +64,7 @@ export class AuthRouter {
64
64
  },
65
65
  can: () => !this.auth.user,
66
66
  lazy: () => import("./components/Register.tsx"),
67
- resolve: async ({ query }) => {
67
+ loader: async ({ query }) => {
68
68
  return {
69
69
  realmConfig: await this.loadRealmConfig(query.realm),
70
70
  };
@@ -81,7 +81,7 @@ export class AuthRouter {
81
81
  },
82
82
  can: () => !this.auth.user,
83
83
  lazy: () => import("./components/ResetPassword.tsx"),
84
- resolve: async ({ query }) => {
84
+ loader: async ({ query }) => {
85
85
  return {
86
86
  realmConfig: await this.loadRealmConfig(query.realm),
87
87
  };
@@ -109,7 +109,7 @@ export class AuthRouter {
109
109
  can: () => !!this.auth.user,
110
110
  path: "/logout",
111
111
  component: () => null,
112
- resolve: () => {
112
+ loader: () => {
113
113
  this.auth.logout();
114
114
  return {};
115
115
  },
@@ -92,6 +92,7 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
92
92
  skipFormElement = false,
93
93
  skipSubmitButton = false,
94
94
  submitButtonProps,
95
+ fill = true,
95
96
  } = props;
96
97
 
97
98
  const schema = props.schema || form.options.schema;
@@ -176,7 +177,7 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
176
177
  <Flex
177
178
  direction={"column"}
178
179
  gap={"sm"}
179
- flex={props.fill ? 1 : undefined}
180
+ flex={fill ? 1 : undefined}
180
181
  {...props.flexProps}
181
182
  >
182
183
  <Flex direction={"column"} gap={"sm"} flex={1}>
@@ -212,7 +213,7 @@ const TypeForm = <T extends TObject>(props: TypeFormProps<T>) => {
212
213
  return (
213
214
  <Flex
214
215
  component={"form"}
215
- flex={props.fill ? 1 : undefined}
216
+ flex={fill ? 1 : undefined}
216
217
  {...form.props}
217
218
  {...props.flexProps}
218
219
  >
@@ -11,6 +11,7 @@ import { ModalsProvider, type ModalsProviderProps } from "@mantine/modals";
11
11
  import { Notifications, type NotificationsProps } from "@mantine/notifications";
12
12
  import type { NavigationProgressProps } from "@mantine/nprogress";
13
13
  import { NavigationProgress, nprogress } from "@mantine/nprogress";
14
+ import { TypeBoxError } from "alepha";
14
15
  import type { ReactNode } from "react";
15
16
  import { useTheme } from "../../hooks/useTheme.ts";
16
17
  import { useToast } from "../../hooks/useToast.ts";
@@ -39,7 +40,10 @@ const AlephaMantineProvider = (props: AlephaMantineProviderProps) => {
39
40
  nprogress.complete();
40
41
  },
41
42
  "react:action:error": ({ error }) => {
42
- if (error instanceof FormValidationError) {
43
+ if (
44
+ error instanceof FormValidationError ||
45
+ error instanceof TypeBoxError
46
+ ) {
43
47
  // Validation errors are handled by the form component
44
48
  return;
45
49
  }
@@ -129,12 +129,14 @@ export const Sidebar = (props: SidebarProps) => {
129
129
  const getSidebarNodes = (): SidebarNode[] => {
130
130
  if (props.items) return props.items;
131
131
  if (props.autoPopulateMenu) {
132
- const items = router.concretePages.map((page) => ({
133
- label: page.label ?? page.name,
134
- //description: page.description?.slice(0, 32),
135
- icon: renderIcon(page.icon),
136
- href: router.path(page.name),
137
- })) as SidebarMenuItem[];
132
+ const items = router.concretePages
133
+ .filter((page) => !page.can || page.can())
134
+ .map((page) => ({
135
+ label: page.label ?? page.name,
136
+ //description: page.description?.slice(0, 32),
137
+ icon: renderIcon(page.icon),
138
+ href: router.path(page.name),
139
+ })) as SidebarMenuItem[];
138
140
  if (
139
141
  typeof props.autoPopulateMenu === "object" &&
140
142
  props.autoPopulateMenu.startsWith
@@ -142,6 +144,7 @@ export const Sidebar = (props: SidebarProps) => {
142
144
  const startsWith = props.autoPopulateMenu.startsWith;
143
145
  return items.filter((item) => item.href?.startsWith(startsWith));
144
146
  }
147
+ return items;
145
148
  }
146
149
  return [];
147
150
  };