@hed-hog/core 0.0.347 → 0.0.350
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.
- package/dist/auth/auth.controller.d.ts +1 -1
- package/dist/auth/auth.service.d.ts +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -0
- package/dist/core.module.js.map +1 -1
- package/dist/dashboard/dashboard-user/dashboard-user.controller.d.ts +3 -3
- package/dist/dashboard/dashboard-user/dashboard-user.service.d.ts +3 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/guards/mcp-auth.guard.d.ts +6 -1
- package/dist/mcp/guards/mcp-auth.guard.d.ts.map +1 -1
- package/dist/mcp/guards/mcp-auth.guard.js +29 -2
- package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
- package/dist/mcp/mcp-api-key.controller.d.ts +32 -0
- package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -0
- package/dist/mcp/mcp-api-key.controller.js +64 -0
- package/dist/mcp/mcp-api-key.controller.js.map +1 -0
- package/dist/mcp/mcp-api-key.service.d.ts +27 -0
- package/dist/mcp/mcp-api-key.service.d.ts.map +1 -0
- package/dist/mcp/mcp-api-key.service.js +74 -0
- package/dist/mcp/mcp-api-key.service.js.map +1 -0
- package/dist/mcp/mcp-registry.service.d.ts +1 -2
- package/dist/mcp/mcp-registry.service.d.ts.map +1 -1
- package/dist/mcp/mcp-registry.service.js +6 -2
- package/dist/mcp/mcp-registry.service.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +8 -1
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.controller.d.ts +4 -0
- package/dist/mcp-chat/mcp-chat.controller.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.controller.js +46 -0
- package/dist/mcp-chat/mcp-chat.controller.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts +6 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +125 -17
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/notification/dto/create-notification.dto.d.ts +26 -0
- package/dist/notification/dto/create-notification.dto.d.ts.map +1 -0
- package/dist/notification/dto/create-notification.dto.js +86 -0
- package/dist/notification/dto/create-notification.dto.js.map +1 -0
- package/dist/notification/dto/update-progress.dto.d.ts +6 -0
- package/dist/notification/dto/update-progress.dto.d.ts.map +1 -0
- package/dist/notification/dto/update-progress.dto.js +33 -0
- package/dist/notification/dto/update-progress.dto.js.map +1 -0
- package/dist/notification/notification.controller.d.ts +108 -0
- package/dist/notification/notification.controller.d.ts.map +1 -0
- package/dist/notification/notification.controller.js +118 -0
- package/dist/notification/notification.controller.js.map +1 -0
- package/dist/notification/notification.module.d.ts +3 -0
- package/dist/notification/notification.module.d.ts.map +1 -0
- package/dist/notification/notification.module.js +29 -0
- package/dist/notification/notification.module.js.map +1 -0
- package/dist/notification/notification.service.d.ts +98 -0
- package/dist/notification/notification.service.d.ts.map +1 -0
- package/dist/notification/notification.service.js +123 -0
- package/dist/notification/notification.service.js.map +1 -0
- package/dist/profile/profile.controller.d.ts +1 -1
- package/dist/profile/profile.service.d.ts +1 -1
- package/dist/setting/dto/update-setting-list.dto.d.ts +9 -0
- package/dist/setting/dto/update-setting-list.dto.d.ts.map +1 -0
- package/dist/setting/dto/update-setting-list.dto.js +44 -0
- package/dist/setting/dto/update-setting-list.dto.js.map +1 -0
- package/dist/setting/setting.controller.d.ts +19 -0
- package/dist/setting/setting.controller.d.ts.map +1 -1
- package/dist/setting/setting.controller.js +12 -0
- package/dist/setting/setting.controller.js.map +1 -1
- package/dist/setting/setting.service.d.ts +19 -0
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +79 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +12 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +60 -0
- package/dist/task/task.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +2 -2
- package/dist/user/user.service.d.ts +3 -3
- package/dist/webhook-integration/webhook-integration.service.d.ts +4 -0
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +184 -34
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/dist/webhook-integration/webhook-utils.d.ts +1 -0
- package/dist/webhook-integration/webhook-utils.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-utils.js +38 -0
- package/dist/webhook-integration/webhook-utils.js.map +1 -1
- package/hedhog/data/menu.yaml +48 -4
- package/hedhog/data/route.yaml +133 -0
- package/hedhog/data/setting_group.yaml +45 -0
- package/hedhog/data/setting_subgroup.yaml +16 -0
- package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +285 -0
- package/hedhog/frontend/app/account/layout.tsx.ejs +32 -55
- package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +5 -0
- package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +2098 -1835
- package/hedhog/frontend/app/mcp_chat/chat-components.tsx.ejs +595 -0
- package/hedhog/frontend/app/mcp_chat/chat-types.ts.ejs +16 -0
- package/hedhog/frontend/app/mcp_chat/page.tsx.ejs +554 -383
- package/hedhog/frontend/app/setting-list/page.tsx.ejs +649 -0
- package/hedhog/frontend/app/users/page.tsx.ejs +445 -203
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +16 -37
- package/hedhog/frontend/messages/en.json +241 -2
- package/hedhog/frontend/messages/pt.json +241 -2
- package/hedhog/table/mcp_api_key.yaml +30 -0
- package/hedhog/table/notification.yaml +72 -0
- package/hedhog/table/webhook_action_log.yaml +49 -0
- package/package.json +2 -2
- package/src/core.module.ts +3 -0
- package/src/index.ts +5 -0
- package/src/mcp/guards/mcp-auth.guard.ts +64 -32
- package/src/mcp/mcp-api-key.controller.ts +42 -0
- package/src/mcp/mcp-api-key.service.ts +66 -0
- package/src/mcp/mcp-registry.service.ts +75 -69
- package/src/mcp/mcp.module.ts +43 -36
- package/src/mcp-chat/mcp-chat.controller.ts +113 -60
- package/src/mcp-chat/mcp-chat.service.ts +494 -332
- package/src/notification/dto/create-notification.dto.ts +71 -0
- package/src/notification/dto/update-progress.dto.ts +16 -0
- package/src/notification/notification.controller.ts +71 -0
- package/src/notification/notification.module.ts +16 -0
- package/src/notification/notification.service.ts +124 -0
- package/src/setting/dto/update-setting-list.dto.ts +32 -0
- package/src/setting/setting.controller.ts +9 -0
- package/src/setting/setting.service.ts +91 -0
- package/src/task/task.service.ts +72 -0
- package/src/webhook-integration/webhook-integration.service.ts +934 -735
- package/src/webhook-integration/webhook-utils.ts +136 -79
|
@@ -6,25 +6,62 @@ import {
|
|
|
6
6
|
PageHeader,
|
|
7
7
|
PaginationFooter,
|
|
8
8
|
SearchBar,
|
|
9
|
-
StatsCards,
|
|
10
9
|
} from '@/components/entity-list';
|
|
10
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
11
|
+
import { Badge } from '@/components/ui/badge';
|
|
11
12
|
import { Button } from '@/components/ui/button';
|
|
12
13
|
import {
|
|
13
14
|
Card,
|
|
15
|
+
CardContent,
|
|
14
16
|
CardDescription,
|
|
15
17
|
CardHeader,
|
|
16
18
|
CardTitle,
|
|
17
19
|
} from '@/components/ui/card';
|
|
20
|
+
import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
|
|
21
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
22
|
+
import {
|
|
23
|
+
Table,
|
|
24
|
+
TableBody,
|
|
25
|
+
TableCell,
|
|
26
|
+
TableHead,
|
|
27
|
+
TableHeader,
|
|
28
|
+
TableRow,
|
|
29
|
+
} from '@/components/ui/table';
|
|
30
|
+
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
|
31
|
+
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
18
32
|
import { getPhotoUrl } from '@/lib/get-photo-url';
|
|
19
33
|
import { getUserEmail } from '@/lib/get-user-email';
|
|
20
34
|
import { PaginatedResult } from '@/types/pagination-result';
|
|
21
35
|
import { User, UserMfa } from '@hed-hog/api-types';
|
|
22
36
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
23
|
-
import {
|
|
24
|
-
|
|
37
|
+
import {
|
|
38
|
+
type ColumnDef,
|
|
39
|
+
flexRender,
|
|
40
|
+
getCoreRowModel,
|
|
41
|
+
useReactTable,
|
|
42
|
+
} from '@tanstack/react-table';
|
|
43
|
+
import {
|
|
44
|
+
LayoutGrid,
|
|
45
|
+
List,
|
|
46
|
+
Mail,
|
|
47
|
+
MoreHorizontal,
|
|
48
|
+
Pencil,
|
|
49
|
+
ShieldCheck,
|
|
50
|
+
UserPlus,
|
|
51
|
+
Users,
|
|
52
|
+
} from 'lucide-react';
|
|
25
53
|
import { useTranslations } from 'next-intl';
|
|
26
|
-
import { useState } from 'react';
|
|
54
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
27
55
|
import { UserSheet } from './user-sheet';
|
|
56
|
+
import {
|
|
57
|
+
DropdownMenu,
|
|
58
|
+
DropdownMenuContent,
|
|
59
|
+
DropdownMenuItem,
|
|
60
|
+
DropdownMenuTrigger,
|
|
61
|
+
} from '@/components/ui/dropdown-menu';
|
|
62
|
+
|
|
63
|
+
const USER_VIEW_STORAGE_KEY = 'core-user-view-mode';
|
|
64
|
+
type UserViewMode = 'table' | 'cards';
|
|
28
65
|
|
|
29
66
|
type RequestResponse<T> = {
|
|
30
67
|
paginate: PaginatedResult<T>;
|
|
@@ -42,6 +79,7 @@ export default function UserPage() {
|
|
|
42
79
|
const [searchQuery, setSearchQuery] = useState('');
|
|
43
80
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
44
81
|
const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
|
|
82
|
+
const [viewMode, setViewMode] = useState<UserViewMode>('table');
|
|
45
83
|
|
|
46
84
|
const [page, setPage] = useState(1);
|
|
47
85
|
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
@@ -51,6 +89,25 @@ export default function UserPage() {
|
|
|
51
89
|
});
|
|
52
90
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
53
91
|
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
try {
|
|
94
|
+
const saved = window.localStorage.getItem(USER_VIEW_STORAGE_KEY);
|
|
95
|
+
if (saved === 'table' || saved === 'cards') setViewMode(saved);
|
|
96
|
+
} catch {
|
|
97
|
+
// Ignore storage read failures.
|
|
98
|
+
}
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const handleViewModeChange = (value: string) => {
|
|
102
|
+
if (value !== 'table' && value !== 'cards') return;
|
|
103
|
+
setViewMode(value);
|
|
104
|
+
try {
|
|
105
|
+
window.localStorage.setItem(USER_VIEW_STORAGE_KEY, value);
|
|
106
|
+
} catch {
|
|
107
|
+
// Ignore storage write failures.
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
54
111
|
const {
|
|
55
112
|
data: { paginate, stats } = {
|
|
56
113
|
paginate: {},
|
|
@@ -98,6 +155,137 @@ export default function UserPage() {
|
|
|
98
155
|
if (!open) setSelectedUserId(null);
|
|
99
156
|
};
|
|
100
157
|
|
|
158
|
+
const statsCards = useMemo(
|
|
159
|
+
() => [
|
|
160
|
+
{
|
|
161
|
+
key: 'total',
|
|
162
|
+
title: t('totalUsers'),
|
|
163
|
+
value: stats?.total ?? 0,
|
|
164
|
+
icon: Users,
|
|
165
|
+
accentClassName: 'from-purple-500/20 via-purple-400/10 to-transparent',
|
|
166
|
+
iconContainerClassName: 'bg-purple-50 text-purple-600',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: 'new7days',
|
|
170
|
+
title: t('totalNewUsers7Days'),
|
|
171
|
+
value: stats?.newLast7Days ?? 0,
|
|
172
|
+
icon: UserPlus,
|
|
173
|
+
accentClassName: 'from-rose-500/20 via-rose-400/10 to-transparent',
|
|
174
|
+
iconContainerClassName: 'bg-rose-50 text-rose-600',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
key: 'validated',
|
|
178
|
+
title: t('totalValidatedUsers'),
|
|
179
|
+
value: 0,
|
|
180
|
+
icon: Mail,
|
|
181
|
+
accentClassName: 'from-green-500/20 via-emerald-400/10 to-transparent',
|
|
182
|
+
iconContainerClassName: 'bg-green-50 text-green-600',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
key: 'blocked',
|
|
186
|
+
title: t('totalBlockedUsers'),
|
|
187
|
+
value: stats?.blocked ?? 0,
|
|
188
|
+
icon: ShieldCheck,
|
|
189
|
+
accentClassName: 'from-amber-500/20 via-orange-400/10 to-transparent',
|
|
190
|
+
iconContainerClassName: 'bg-amber-50 text-amber-600',
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
[stats, t]
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const columns = useMemo<ColumnDef<User>[]>(
|
|
197
|
+
() => [
|
|
198
|
+
{
|
|
199
|
+
accessorKey: 'name',
|
|
200
|
+
header: t('columnName'),
|
|
201
|
+
cell: ({ row }) => {
|
|
202
|
+
const user = row.original;
|
|
203
|
+
return (
|
|
204
|
+
<div className="flex items-center gap-3">
|
|
205
|
+
<Avatar className="h-8 w-8 rounded-full">
|
|
206
|
+
<AvatarImage
|
|
207
|
+
src={getPhotoUrl(user.photo_id)}
|
|
208
|
+
alt={user.name}
|
|
209
|
+
className="rounded-full object-cover"
|
|
210
|
+
/>
|
|
211
|
+
<AvatarFallback className="rounded-full bg-muted text-xs font-semibold uppercase">
|
|
212
|
+
{(user.name ?? '?').slice(0, 2).toUpperCase()}
|
|
213
|
+
</AvatarFallback>
|
|
214
|
+
</Avatar>
|
|
215
|
+
<span className="font-medium">{user.name || '—'}</span>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
accessorKey: 'email',
|
|
222
|
+
header: t('columnEmail'),
|
|
223
|
+
cell: ({ row }) => (
|
|
224
|
+
<span className="text-sm text-muted-foreground">
|
|
225
|
+
{getUserEmail(row.original)}
|
|
226
|
+
</span>
|
|
227
|
+
),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
accessorKey: 'status',
|
|
231
|
+
header: t('columnStatus'),
|
|
232
|
+
cell: ({ row }) => {
|
|
233
|
+
const user = row.original;
|
|
234
|
+
return user.suspended_until ? (
|
|
235
|
+
<Badge
|
|
236
|
+
variant="outline"
|
|
237
|
+
className="border-red-500/20 bg-red-500/10 text-red-600"
|
|
238
|
+
>
|
|
239
|
+
{t('blocked')}
|
|
240
|
+
</Badge>
|
|
241
|
+
) : (
|
|
242
|
+
<Badge
|
|
243
|
+
variant="outline"
|
|
244
|
+
className="border-green-500/20 bg-green-500/10 text-green-600"
|
|
245
|
+
>
|
|
246
|
+
{t('active')}
|
|
247
|
+
</Badge>
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: 'actions',
|
|
253
|
+
header: '',
|
|
254
|
+
cell: ({ row }) => (
|
|
255
|
+
<div className="flex justify-end">
|
|
256
|
+
<DropdownMenu>
|
|
257
|
+
<DropdownMenuTrigger asChild>
|
|
258
|
+
<Button variant="ghost" size="icon" className="h-8 w-8">
|
|
259
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
260
|
+
</Button>
|
|
261
|
+
</DropdownMenuTrigger>
|
|
262
|
+
<DropdownMenuContent align="end">
|
|
263
|
+
<DropdownMenuItem onClick={() => handleOpenEdit(row.original)}>
|
|
264
|
+
<Pencil className="mr-2 h-4 w-4" />
|
|
265
|
+
{t('buttonEditUser')}
|
|
266
|
+
</DropdownMenuItem>
|
|
267
|
+
</DropdownMenuContent>
|
|
268
|
+
</DropdownMenu>
|
|
269
|
+
</div>
|
|
270
|
+
),
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
[t]
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const tableData = useMemo(
|
|
277
|
+
() => (paginate as PaginatedResult<User>)?.data ?? [],
|
|
278
|
+
[paginate]
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const table = useReactTable({
|
|
282
|
+
data: tableData,
|
|
283
|
+
columns,
|
|
284
|
+
getCoreRowModel: getCoreRowModel(),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const users: User[] = tableData;
|
|
288
|
+
|
|
101
289
|
return (
|
|
102
290
|
<Page>
|
|
103
291
|
<PageHeader
|
|
@@ -113,91 +301,94 @@ export default function UserPage() {
|
|
|
113
301
|
description={t('description')}
|
|
114
302
|
/>
|
|
115
303
|
|
|
116
|
-
<
|
|
117
|
-
stats={[
|
|
118
|
-
{
|
|
119
|
-
title: t('totalUsers'),
|
|
120
|
-
value: String(stats?.total || 0),
|
|
121
|
-
icon: <Users className="h-5 w-5" />,
|
|
122
|
-
iconBgColor: 'bg-purple-50',
|
|
123
|
-
iconColor: 'text-purple-600',
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
title: t('totalNewUsers7Days'),
|
|
127
|
-
value: String(stats?.newLast7Days || 0),
|
|
128
|
-
icon: <UserPlus className="h-5 w-5" />,
|
|
129
|
-
iconBgColor: 'bg-rose-50',
|
|
130
|
-
iconColor: 'text-rose-600',
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
title: t('totalValidatedUsers'),
|
|
134
|
-
value: '0',
|
|
135
|
-
icon: <Mail className="h-5 w-5" />,
|
|
136
|
-
iconBgColor: 'bg-green-50',
|
|
137
|
-
iconColor: 'text-green-600',
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
title: t('totalBlockedUsers'),
|
|
141
|
-
value: String(stats?.blocked || 0),
|
|
142
|
-
icon: <ShieldCheck className="h-5 w-5" />,
|
|
143
|
-
iconBgColor: 'bg-amber-50',
|
|
144
|
-
iconColor: 'text-amber-600',
|
|
145
|
-
},
|
|
146
|
-
]}
|
|
147
|
-
/>
|
|
304
|
+
<KpiCardsGrid items={statsCards} />
|
|
148
305
|
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
306
|
+
<div className="flex flex-col gap-4 xl:flex-row xl:items-center">
|
|
307
|
+
<div className="flex-1">
|
|
308
|
+
<SearchBar
|
|
309
|
+
searchQuery={searchQuery}
|
|
310
|
+
onSearchChange={(v) => {
|
|
311
|
+
setSearchQuery(v);
|
|
312
|
+
setPage(1);
|
|
313
|
+
}}
|
|
314
|
+
onSearch={() => refetch()}
|
|
315
|
+
placeholder={t('searchPlaceholder')}
|
|
316
|
+
filters={{
|
|
317
|
+
value: statusFilter,
|
|
318
|
+
onChange: (value) => {
|
|
319
|
+
setStatusFilter(value);
|
|
320
|
+
setPage(1);
|
|
321
|
+
},
|
|
322
|
+
placeholder: t('filterPlaceholder'),
|
|
323
|
+
options: [
|
|
324
|
+
{ label: t('filterOptionAll'), value: 'all' },
|
|
325
|
+
{ label: t('filterOptionNew'), value: 'new' },
|
|
326
|
+
{ label: t('filterOptionBlocked'), value: 'blocked' },
|
|
327
|
+
],
|
|
328
|
+
}}
|
|
329
|
+
/>
|
|
330
|
+
</div>
|
|
168
331
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
</
|
|
196
|
-
|
|
197
|
-
</
|
|
198
|
-
|
|
332
|
+
<div className="flex items-center gap-3 xl:justify-end">
|
|
333
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
334
|
+
{t('viewMode')}
|
|
335
|
+
</span>
|
|
336
|
+
<ToggleGroup
|
|
337
|
+
type="single"
|
|
338
|
+
value={viewMode}
|
|
339
|
+
onValueChange={handleViewModeChange}
|
|
340
|
+
variant="outline"
|
|
341
|
+
size="sm"
|
|
342
|
+
aria-label={t('viewMode')}
|
|
343
|
+
>
|
|
344
|
+
<ToggleGroupItem
|
|
345
|
+
value="table"
|
|
346
|
+
className="gap-1.5 px-2.5"
|
|
347
|
+
aria-label={t('viewModeTable')}
|
|
348
|
+
>
|
|
349
|
+
<List className="h-4 w-4" />
|
|
350
|
+
<span className="hidden sm:inline">{t('viewModeTable')}</span>
|
|
351
|
+
</ToggleGroupItem>
|
|
352
|
+
<ToggleGroupItem
|
|
353
|
+
value="cards"
|
|
354
|
+
className="gap-1.5 px-2.5"
|
|
355
|
+
aria-label={t('viewModeCards')}
|
|
356
|
+
>
|
|
357
|
+
<LayoutGrid className="h-4 w-4" />
|
|
358
|
+
<span className="hidden sm:inline">{t('viewModeCards')}</span>
|
|
359
|
+
</ToggleGroupItem>
|
|
360
|
+
</ToggleGroup>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
199
363
|
|
|
200
|
-
|
|
364
|
+
<div className="flex-1">
|
|
365
|
+
{isLoading ? (
|
|
366
|
+
viewMode === 'cards' ? (
|
|
367
|
+
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
|
368
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
369
|
+
<Card key={i} className="overflow-hidden py-0">
|
|
370
|
+
<CardContent className="space-y-3 p-4">
|
|
371
|
+
<div className="flex items-center gap-3">
|
|
372
|
+
<Skeleton className="h-12 w-12 rounded-full" />
|
|
373
|
+
<div className="flex-1 space-y-2">
|
|
374
|
+
<Skeleton className="h-4 w-40" />
|
|
375
|
+
<Skeleton className="h-3 w-32" />
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
<Skeleton className="h-3 w-full" />
|
|
379
|
+
<Skeleton className="h-3 w-1/2" />
|
|
380
|
+
</CardContent>
|
|
381
|
+
</Card>
|
|
382
|
+
))}
|
|
383
|
+
</div>
|
|
384
|
+
) : (
|
|
385
|
+
<div className="space-y-3">
|
|
386
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
387
|
+
<Skeleton key={i} className="h-14 w-full" />
|
|
388
|
+
))}
|
|
389
|
+
</div>
|
|
390
|
+
)
|
|
391
|
+
) : users.length === 0 ? (
|
|
201
392
|
<EmptyState
|
|
202
393
|
icon={<Users className="h-12 w-12" />}
|
|
203
394
|
title={t('noUsersFound')}
|
|
@@ -205,141 +396,192 @@ export default function UserPage() {
|
|
|
205
396
|
actionLabel={t('buttonAddUser')}
|
|
206
397
|
onAction={handleOpenCreate}
|
|
207
398
|
/>
|
|
399
|
+
) : viewMode === 'table' ? (
|
|
400
|
+
<div className="overflow-x-auto">
|
|
401
|
+
<Table>
|
|
402
|
+
<TableHeader>
|
|
403
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
404
|
+
<TableRow key={headerGroup.id}>
|
|
405
|
+
{headerGroup.headers.map((header) => (
|
|
406
|
+
<TableHead key={header.id}>
|
|
407
|
+
{header.isPlaceholder
|
|
408
|
+
? null
|
|
409
|
+
: flexRender(
|
|
410
|
+
header.column.columnDef.header,
|
|
411
|
+
header.getContext()
|
|
412
|
+
)}
|
|
413
|
+
</TableHead>
|
|
414
|
+
))}
|
|
415
|
+
</TableRow>
|
|
416
|
+
))}
|
|
417
|
+
</TableHeader>
|
|
418
|
+
<TableBody>
|
|
419
|
+
{table.getRowModel().rows.map((row) => (
|
|
420
|
+
<TableRow
|
|
421
|
+
key={row.id}
|
|
422
|
+
className="cursor-pointer"
|
|
423
|
+
onDoubleClick={() => handleOpenEdit(row.original)}
|
|
424
|
+
>
|
|
425
|
+
{row.getVisibleCells().map((cell) => (
|
|
426
|
+
<TableCell key={cell.id}>
|
|
427
|
+
{flexRender(
|
|
428
|
+
cell.column.columnDef.cell,
|
|
429
|
+
cell.getContext()
|
|
430
|
+
)}
|
|
431
|
+
</TableCell>
|
|
432
|
+
))}
|
|
433
|
+
</TableRow>
|
|
434
|
+
))}
|
|
435
|
+
</TableBody>
|
|
436
|
+
</Table>
|
|
437
|
+
</div>
|
|
208
438
|
) : (
|
|
209
|
-
<div className="grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
|
210
|
-
{
|
|
439
|
+
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3">
|
|
440
|
+
{users.map((user: User) => (
|
|
211
441
|
<Card
|
|
212
442
|
key={user.id}
|
|
213
443
|
onDoubleClick={() => handleOpenEdit(user)}
|
|
214
|
-
className="cursor-pointer
|
|
444
|
+
className="cursor-pointer overflow-hidden border-border/70 py-0 transition-colors hover:border-border hover:shadow-md"
|
|
215
445
|
>
|
|
216
|
-
<
|
|
217
|
-
<
|
|
218
|
-
<div className="
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
446
|
+
<CardContent className="flex flex-col gap-3 p-4">
|
|
447
|
+
<CardHeader className="flex items-start justify-between gap-4 p-0">
|
|
448
|
+
<div className="flex items-center gap-3">
|
|
449
|
+
<Avatar className="h-12 w-12 shrink-0 rounded-full">
|
|
450
|
+
<AvatarImage
|
|
451
|
+
src={getPhotoUrl(user.photo_id)}
|
|
452
|
+
alt={user.name}
|
|
453
|
+
className="rounded-full object-cover"
|
|
454
|
+
/>
|
|
455
|
+
<AvatarFallback className="rounded-full bg-muted text-sm font-semibold uppercase">
|
|
456
|
+
{(user.name ?? '?').slice(0, 2).toUpperCase()}
|
|
457
|
+
</AvatarFallback>
|
|
458
|
+
</Avatar>
|
|
459
|
+
<div>
|
|
460
|
+
<CardTitle className="text-sm font-semibold">
|
|
461
|
+
{user.name || '—'}
|
|
462
|
+
</CardTitle>
|
|
463
|
+
<CardDescription className="text-xs text-muted-foreground">
|
|
464
|
+
{getUserEmail(user)}
|
|
465
|
+
</CardDescription>
|
|
466
|
+
</div>
|
|
224
467
|
</div>
|
|
225
|
-
<div>
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
468
|
+
<div className="flex flex-col items-end gap-2">
|
|
469
|
+
<Button
|
|
470
|
+
variant="outline"
|
|
471
|
+
size="sm"
|
|
472
|
+
onClick={() => handleOpenEdit(user)}
|
|
473
|
+
>
|
|
474
|
+
{t('buttonEditUser')}
|
|
475
|
+
</Button>
|
|
476
|
+
<div className="mt-1">
|
|
477
|
+
{user.suspended_until ? (
|
|
478
|
+
<Badge
|
|
479
|
+
variant="outline"
|
|
480
|
+
className="border-red-500/20 bg-red-500/10 text-red-600"
|
|
481
|
+
>
|
|
482
|
+
<ShieldCheck className="mr-1 h-3 w-3" />
|
|
483
|
+
{t('blocked')}
|
|
484
|
+
</Badge>
|
|
485
|
+
) : (
|
|
486
|
+
<Badge
|
|
487
|
+
variant="outline"
|
|
488
|
+
className="border-green-500/20 bg-green-500/10 text-green-600"
|
|
489
|
+
>
|
|
490
|
+
<Users className="mr-1 h-3 w-3" />
|
|
491
|
+
{t('active')}
|
|
492
|
+
</Badge>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
232
495
|
</div>
|
|
233
|
-
</
|
|
234
|
-
<div className="flex flex-col items-end gap-2">
|
|
235
|
-
<Button
|
|
236
|
-
variant="outline"
|
|
237
|
-
size="sm"
|
|
238
|
-
onClick={() => handleOpenEdit(user)}
|
|
239
|
-
>
|
|
240
|
-
{t('buttonEditUser')}
|
|
241
|
-
</Button>
|
|
496
|
+
</CardHeader>
|
|
242
497
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{t('
|
|
248
|
-
|
|
498
|
+
<div className="space-y-2 text-sm text-muted-foreground">
|
|
499
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
500
|
+
{(user as any).user_account?.includes('google') && (
|
|
501
|
+
<div
|
|
502
|
+
title={t('googleConnected')}
|
|
503
|
+
className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
|
|
504
|
+
>
|
|
505
|
+
<svg
|
|
506
|
+
className="h-4 w-4"
|
|
507
|
+
viewBox="0 0 24 24"
|
|
508
|
+
fill="currentColor"
|
|
509
|
+
aria-hidden
|
|
510
|
+
/>
|
|
511
|
+
{t('google')}
|
|
512
|
+
</div>
|
|
513
|
+
)}
|
|
514
|
+
{(user as any).connectedAccounts?.includes('github') && (
|
|
515
|
+
<div
|
|
516
|
+
title={t('githubConnected')}
|
|
517
|
+
className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
|
|
518
|
+
>
|
|
519
|
+
<svg
|
|
520
|
+
className="h-4 w-4"
|
|
521
|
+
viewBox="0 0 24 24"
|
|
522
|
+
fill="currentColor"
|
|
523
|
+
aria-hidden
|
|
524
|
+
/>
|
|
525
|
+
{t('github')}
|
|
526
|
+
</div>
|
|
527
|
+
)}
|
|
528
|
+
{(user as any).connectedAccounts?.includes(
|
|
529
|
+
'facebook'
|
|
530
|
+
) && (
|
|
531
|
+
<div
|
|
532
|
+
title={t('facebookConnected')}
|
|
533
|
+
className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
|
|
534
|
+
>
|
|
535
|
+
<svg
|
|
536
|
+
className="h-4 w-4"
|
|
537
|
+
viewBox="0 0 24 24"
|
|
538
|
+
fill="currentColor"
|
|
539
|
+
aria-hidden
|
|
540
|
+
/>
|
|
541
|
+
{t('facebook')}
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
{(!(user as any).connectedAccounts ||
|
|
545
|
+
(user as any).connectedAccounts.length === 0) && (
|
|
546
|
+
<div className="text-xs text-muted-foreground">
|
|
547
|
+
{t('noConnectedAccounts')}
|
|
548
|
+
</div>
|
|
549
|
+
)}
|
|
550
|
+
</div>
|
|
551
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
552
|
+
{user.user_mfa && user.user_mfa.length > 0 ? (
|
|
553
|
+
user.user_mfa.map((mfa: UserMfa) => (
|
|
554
|
+
<span
|
|
555
|
+
key={String(mfa.id)}
|
|
556
|
+
className="rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700"
|
|
557
|
+
>
|
|
558
|
+
{mfa.name}
|
|
559
|
+
</span>
|
|
560
|
+
))
|
|
249
561
|
) : (
|
|
250
|
-
<span className="
|
|
251
|
-
|
|
252
|
-
{t('active')}
|
|
562
|
+
<span className="text-xs text-muted-foreground">
|
|
563
|
+
{t('noMfaMethods')}
|
|
253
564
|
</span>
|
|
254
565
|
)}
|
|
255
566
|
</div>
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
title={t('googleConnected')}
|
|
263
|
-
className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
|
|
264
|
-
>
|
|
265
|
-
<svg
|
|
266
|
-
className="h-4 w-4"
|
|
267
|
-
viewBox="0 0 24 24"
|
|
268
|
-
fill="currentColor"
|
|
269
|
-
aria-hidden
|
|
270
|
-
/>
|
|
271
|
-
{t('google')}
|
|
272
|
-
</div>
|
|
273
|
-
)}
|
|
274
|
-
{(user as any).connectedAccounts?.includes('github') && (
|
|
275
|
-
<div
|
|
276
|
-
title={t('githubConnected')}
|
|
277
|
-
className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
|
|
278
|
-
>
|
|
279
|
-
<svg
|
|
280
|
-
className="h-4 w-4"
|
|
281
|
-
viewBox="0 0 24 24"
|
|
282
|
-
fill="currentColor"
|
|
283
|
-
aria-hidden
|
|
284
|
-
/>
|
|
285
|
-
{t('github')}
|
|
286
|
-
</div>
|
|
287
|
-
)}
|
|
288
|
-
{(user as any).connectedAccounts?.includes('facebook') && (
|
|
289
|
-
<div
|
|
290
|
-
title={t('facebookConnected')}
|
|
291
|
-
className="inline-flex items-center gap-2 rounded-md bg-slate-50 px-2 py-1 text-xs text-slate-700"
|
|
292
|
-
>
|
|
293
|
-
<svg
|
|
294
|
-
className="h-4 w-4"
|
|
295
|
-
viewBox="0 0 24 24"
|
|
296
|
-
fill="currentColor"
|
|
297
|
-
aria-hidden
|
|
298
|
-
/>
|
|
299
|
-
{t('facebook')}
|
|
567
|
+
{user.suspended_reason && (
|
|
568
|
+
<div className="text-xs text-muted-foreground">
|
|
569
|
+
<span className="font-medium text-foreground">
|
|
570
|
+
{t('reason')}:
|
|
571
|
+
</span>{' '}
|
|
572
|
+
{user.suspended_reason}
|
|
300
573
|
</div>
|
|
301
574
|
)}
|
|
302
|
-
{
|
|
303
|
-
(user as any).connectedAccounts.length === 0) && (
|
|
575
|
+
{user.suspended_until && (
|
|
304
576
|
<div className="text-xs text-muted-foreground">
|
|
305
|
-
|
|
577
|
+
<span className="font-medium text-foreground">
|
|
578
|
+
{t('until')}:
|
|
579
|
+
</span>{' '}
|
|
580
|
+
{user.suspended_until}
|
|
306
581
|
</div>
|
|
307
582
|
)}
|
|
308
583
|
</div>
|
|
309
|
-
|
|
310
|
-
{user.user_mfa && user.user_mfa.length > 0 ? (
|
|
311
|
-
user.user_mfa.map((mfa: UserMfa) => (
|
|
312
|
-
<span
|
|
313
|
-
key={String(mfa.id)}
|
|
314
|
-
className="rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700"
|
|
315
|
-
title={`MFA: ${mfa}`}
|
|
316
|
-
>
|
|
317
|
-
{mfa.name}
|
|
318
|
-
</span>
|
|
319
|
-
))
|
|
320
|
-
) : (
|
|
321
|
-
<span className="text-xs text-muted-foreground">
|
|
322
|
-
{t('noMfaMethods')}
|
|
323
|
-
</span>
|
|
324
|
-
)}
|
|
325
|
-
</div>
|
|
326
|
-
{user.suspended_reason && (
|
|
327
|
-
<div className="text-xs text-muted-foreground">
|
|
328
|
-
<span className="font-medium text-foreground">
|
|
329
|
-
{t('reason')}:
|
|
330
|
-
</span>{' '}
|
|
331
|
-
{user.suspended_reason}
|
|
332
|
-
</div>
|
|
333
|
-
)}
|
|
334
|
-
{user.suspended_until && (
|
|
335
|
-
<div className="text-xs text-muted-foreground">
|
|
336
|
-
<span className="font-medium text-foreground">
|
|
337
|
-
{t('until')}:
|
|
338
|
-
</span>{' '}
|
|
339
|
-
{user.suspended_until}
|
|
340
|
-
</div>
|
|
341
|
-
)}
|
|
342
|
-
</div>
|
|
584
|
+
</CardContent>
|
|
343
585
|
</Card>
|
|
344
586
|
))}
|
|
345
587
|
</div>
|