fix(design-preview): убрать страницу пользователя — дублировала панель прав
Права, сброс passkeys, отключение и удаление уже есть в панели прав и меню строки,
сессии и события — в «Входах» и «Изменениях». Страница откроется из коммита 007ece4.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8351424f4
commit
8bbaab62e4
7 files changed
+4
-373
No files matched your search
@@ -1,5 +1,4 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { ArrowUpRightIcon } from 'lucide-react'
|
|
||||||
import type { AdminUser } from '@authportal/shared'
|
import type { AdminUser } from '@authportal/shared'
|
||||||
import { Button } from '@authportal/ui/components/button'
|
import { Button } from '@authportal/ui/components/button'
|
||||||
import { AccessEditor } from './access-editor'
|
import { AccessEditor } from './access-editor'
|
||||||
@@ -11,12 +10,10 @@ export function AccessSheet({
|
|||||||
user,
|
user,
|
||||||
onClose,
|
onClose,
|
||||||
onSave,
|
onSave,
|
||||||
onOpenProfile,
|
|
||||||
}: {
|
}: {
|
||||||
user: AdminUser
|
user: AdminUser
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSave: (value: AccessValue) => void
|
onSave: (value: AccessValue) => void
|
||||||
onOpenProfile?: () => void
|
|
||||||
}) {
|
}) {
|
||||||
const [value, setValue] = useState<AccessValue>({ apps: user.apps, permissions: user.permissions })
|
const [value, setValue] = useState<AccessValue>({ apps: user.apps, permissions: user.permissions })
|
||||||
const changes = useMemo(() => diffAccess({ apps: user.apps, permissions: user.permissions }, value).length, [user, value])
|
const changes = useMemo(() => diffAccess({ apps: user.apps, permissions: user.permissions }, value).length, [user, value])
|
||||||
@@ -31,12 +28,6 @@ export function AccessSheet({
|
|||||||
description="Доступ к приложениям и разделам · иерархия: админ ⊃ запись ⊃ чтение"
|
description="Доступ к приложениям и разделам · иерархия: админ ⊃ запись ⊃ чтение"
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
{onOpenProfile ? (
|
|
||||||
<Button variant="ghost" onClick={onOpenProfile}>
|
|
||||||
Открыть профиль
|
|
||||||
<ArrowUpRightIcon aria-hidden />
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
<span className="text-muted-foreground mr-auto self-center text-xs">
|
<span className="text-muted-foreground mr-auto self-center text-xs">
|
||||||
{changes > 0 ? `Изменений: ${changes}` : 'Без изменений'}
|
{changes > 0 ? `Изменений: ${changes}` : 'Без изменений'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react'
|
|
||||||
import { Link, useNavigate, useParams } from '@tanstack/react-router'
|
|
||||||
import {
|
|
||||||
ArrowLeftIcon,
|
|
||||||
FingerprintIcon,
|
|
||||||
HistoryIcon,
|
|
||||||
MonitorSmartphoneIcon,
|
|
||||||
ShieldCheckIcon,
|
|
||||||
TriangleAlertIcon,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { toast } from 'sonner'
|
|
||||||
import type { AdminUser } from '@authportal/shared'
|
|
||||||
import { AuditLogTimeline } from '@/components/reui-kit/audit-log-timeline'
|
|
||||||
import { Frame, FrameDescription, FrameFooter, FrameHeader, FramePanel, FrameTitle } from '@/components/reui/frame'
|
|
||||||
import { Avatar, AvatarFallback } from '@authportal/ui/components/avatar'
|
|
||||||
import { Button } from '@authportal/ui/components/button'
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@authportal/ui/components/tabs'
|
|
||||||
import { AccessEditor } from '../access-editor'
|
|
||||||
import { diffAccess, type AccessValue } from '../access-model'
|
|
||||||
import { KvFrame, PendingChip, StatusBadge, TypedConfirm } from '../bits'
|
|
||||||
import { MOCK_AUDIT, MOCK_SESSIONS, USER_META, type SessionRow } from '../mock'
|
|
||||||
import { usePreviewStore } from '../preview-store'
|
|
||||||
import { PreviewSectionGrid, TwoLineCell, type GridColumn } from '../section-grid'
|
|
||||||
import { usePatchUsers } from '../user-actions'
|
|
||||||
|
|
||||||
function initials(u: AdminUser) {
|
|
||||||
const base = (u.name || u.email).trim()
|
|
||||||
const parts = base.split(/\s+/)
|
|
||||||
return (parts.length > 1 ? `${parts[0]![0]}${parts[1]![0]}` : base.slice(0, 2)).toUpperCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
function statusOf(u: AdminUser) {
|
|
||||||
if (u.disabled) return { tone: 'mu' as const, label: 'Отключён' }
|
|
||||||
if (u.passkey_count === 0) return { tone: 'wn' as const, label: 'Без passkey' }
|
|
||||||
return { tone: 'ok' as const, label: 'Активен' }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Страница пользователя (solution-users-2): рейка вкладок — права, вход, сессии, активность, опасная зона. */
|
|
||||||
export function UserProfilePage() {
|
|
||||||
const { userId } = useParams({ from: '/design-preview/_shell/users/$userId' })
|
|
||||||
const { users, setUsers, addChange, marksFor } = usePreviewStore()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const patch = usePatchUsers()
|
|
||||||
const user = users.find((u) => u.id === userId)
|
|
||||||
const mark = marksFor('users')[userId]
|
|
||||||
|
|
||||||
const [draft, setDraft] = useState<{ id: string; value: AccessValue } | null>(null)
|
|
||||||
const [sessions, setSessions] = useState<SessionRow[]>(MOCK_SESSIONS)
|
|
||||||
const [resetOpen, setResetOpen] = useState(false)
|
|
||||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
|
||||||
|
|
||||||
const sessionRows = useMemo(() => sessions.filter((s) => s.user === user?.email), [sessions, user])
|
|
||||||
const events = useMemo(
|
|
||||||
() => MOCK_AUDIT.filter((e) => e.actor_email === user?.email || e.summary.includes(user?.email ?? '\0')),
|
|
||||||
[user],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return (
|
|
||||||
<Frame spacing="sm" className="w-full">
|
|
||||||
<FramePanel className="flex flex-col items-start gap-3 py-8">
|
|
||||||
<p className="font-medium">Пользователь не найден</p>
|
|
||||||
<p className="text-muted-foreground text-sm">Возможно, учётная запись уже удалена.</p>
|
|
||||||
<Button variant="outline" render={<Link to="/design-preview/users" />} nativeButton={false}>
|
|
||||||
<ArrowLeftIcon aria-hidden />К списку пользователей
|
|
||||||
</Button>
|
|
||||||
</FramePanel>
|
|
||||||
</Frame>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = statusOf(user)
|
|
||||||
const base: AccessValue = { apps: user.apps, permissions: user.permissions }
|
|
||||||
const value = draft?.id === user.id ? draft.value : base
|
|
||||||
const changes = diffAccess(base, value).length
|
|
||||||
|
|
||||||
const sessionColumns: GridColumn<SessionRow>[] = [
|
|
||||||
{ id: 'ip', title: 'IP · клиент', size: 260, cell: (r) => <TwoLineCell dot={r.tone} title={r.ipClient} sub={r.note || undefined} /> },
|
|
||||||
{ id: 'created', title: 'Создана', size: 140, cell: (r) => <span className="text-sm whitespace-nowrap">{r.created}</span> },
|
|
||||||
{ id: 'exp', title: 'Истекает', size: 120, cell: (r) => <span className="text-sm whitespace-nowrap">{r.expires}</span> },
|
|
||||||
{
|
|
||||||
id: 'act',
|
|
||||||
title: '',
|
|
||||||
size: 130,
|
|
||||||
cell: (r) => (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setSessions((s) => s.filter((x) => x.id !== r.id))
|
|
||||||
toast.success('Сессия завершена (макет)')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Завершить
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Frame spacing="sm" className="w-full">
|
|
||||||
<FramePanel className="flex flex-wrap items-center gap-x-4 gap-y-3">
|
|
||||||
<Avatar className="size-12 rounded-xl">
|
|
||||||
<AvatarFallback className="rounded-xl text-sm">{initials(user)}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex min-w-0 flex-1 basis-60 flex-col gap-1">
|
|
||||||
<h1 className="truncate text-xl font-semibold tracking-tight">{user.email}</h1>
|
|
||||||
<p className="text-muted-foreground truncate font-mono text-[11px]">{USER_META[user.id]?.sub ?? user.name}</p>
|
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
|
||||||
<StatusBadge tone={status.tone}>{status.label}</StatusBadge>
|
|
||||||
<StatusBadge tone={user.is_admin ? 'wn' : 'in'}>{user.is_admin ? 'Админ' : 'Пользователь'}</StatusBadge>
|
|
||||||
{mark ? <PendingChip mark={mark} /> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" render={<Link to="/design-preview/users" />} nativeButton={false}>
|
|
||||||
<ArrowLeftIcon aria-hidden />К списку
|
|
||||||
</Button>
|
|
||||||
</FramePanel>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
<Tabs defaultValue="access" className="min-w-0 flex-col gap-4 md:flex-row md:items-start">
|
|
||||||
<TabsList className="h-auto! w-full flex-row flex-wrap justify-start md:w-56 md:shrink-0 md:flex-col md:items-stretch">
|
|
||||||
<TabsTrigger value="access" className="justify-start">
|
|
||||||
<ShieldCheckIcon aria-hidden />
|
|
||||||
Права доступа
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="auth" className="justify-start">
|
|
||||||
<FingerprintIcon aria-hidden />
|
|
||||||
Вход и passkeys
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="sessions" className="justify-start">
|
|
||||||
<MonitorSmartphoneIcon aria-hidden />
|
|
||||||
Сессии · {sessionRows.length}
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="activity" className="justify-start">
|
|
||||||
<HistoryIcon aria-hidden />
|
|
||||||
Активность
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="danger" className="justify-start">
|
|
||||||
<TriangleAlertIcon aria-hidden />
|
|
||||||
Опасная зона
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<TabsContent value="access" className="min-w-0">
|
|
||||||
<Frame spacing="sm" className="w-full">
|
|
||||||
<FrameHeader>
|
|
||||||
<FrameTitle>Права доступа</FrameTitle>
|
|
||||||
<FrameDescription>Приложения и разделы · иерархия: админ ⊃ запись ⊃ чтение</FrameDescription>
|
|
||||||
</FrameHeader>
|
|
||||||
<FramePanel>
|
|
||||||
<AccessEditor user={user} value={value} onChange={(next) => setDraft({ id: user.id, value: next })} />
|
|
||||||
</FramePanel>
|
|
||||||
<FrameFooter className="flex-wrap justify-end gap-2">
|
|
||||||
<span className="text-muted-foreground mr-auto text-xs">{changes > 0 ? `Изменений: ${changes}` : 'Без изменений'}</span>
|
|
||||||
<Button variant="outline" disabled={changes === 0} onClick={() => setDraft(null)}>
|
|
||||||
Сбросить
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={changes === 0}
|
|
||||||
onClick={() => {
|
|
||||||
patch(
|
|
||||||
[user.id],
|
|
||||||
(u) => ({ ...u, apps: value.apps, permissions: value.permissions }),
|
|
||||||
`Права обновлены: ${user.email}`,
|
|
||||||
() => diffAccess(base, value),
|
|
||||||
)
|
|
||||||
setDraft(null)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Сохранить права
|
|
||||||
</Button>
|
|
||||||
</FrameFooter>
|
|
||||||
</Frame>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="auth" className="flex min-w-0 flex-col gap-4">
|
|
||||||
<KvFrame
|
|
||||||
title="Вход"
|
|
||||||
description="Способы входа учётной записи"
|
|
||||||
items={[
|
|
||||||
{ label: 'Пароль', value: 'Задан · запасной вход' },
|
|
||||||
{ label: 'Passkeys', value: user.passkey_count === 0 ? 'нет' : `${user.passkey_count} шт.` },
|
|
||||||
{ label: 'Последний вход', value: USER_META[user.id]?.activity ?? '—' },
|
|
||||||
{ label: 'Статус', value: status.label },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Frame spacing="sm" className="w-full">
|
|
||||||
<FramePanel className="flex flex-wrap items-center justify-between gap-3">
|
|
||||||
<div className="flex min-w-0 flex-col">
|
|
||||||
<span className="text-sm font-medium">Сбросить passkeys…</span>
|
|
||||||
<span className="text-muted-foreground text-xs">Войти можно будет только по паролю, пока не добавлен новый ключ.</span>
|
|
||||||
</div>
|
|
||||||
<Button variant="outline" disabled={user.passkey_count === 0} onClick={() => setResetOpen(true)}>
|
|
||||||
Сбросить
|
|
||||||
</Button>
|
|
||||||
</FramePanel>
|
|
||||||
</Frame>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="sessions" className="min-w-0">
|
|
||||||
<PreviewSectionGrid
|
|
||||||
title="Активные сессии"
|
|
||||||
description="Завершение сессии выкидывает пользователя из портала и приложений"
|
|
||||||
rows={sessionRows}
|
|
||||||
columns={sessionColumns}
|
|
||||||
getRowId={(r) => r.id}
|
|
||||||
emptyMessage="Нет активных сессий."
|
|
||||||
/>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="activity" className="min-w-0">
|
|
||||||
<Frame spacing="sm" className="w-full">
|
|
||||||
<FrameHeader>
|
|
||||||
<FrameTitle>Активность</FrameTitle>
|
|
||||||
<FrameDescription>События пользователя из журнала портала</FrameDescription>
|
|
||||||
</FrameHeader>
|
|
||||||
<FramePanel>
|
|
||||||
{events.length === 0 ? (
|
|
||||||
<p className="text-muted-foreground text-sm">Событий нет.</p>
|
|
||||||
) : (
|
|
||||||
<AuditLogTimeline entries={events} totalCount={events.length} compact lockedUserId={user.id} />
|
|
||||||
)}
|
|
||||||
</FramePanel>
|
|
||||||
</Frame>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="danger" className="min-w-0">
|
|
||||||
<Frame spacing="sm" className="w-full">
|
|
||||||
<FrameHeader>
|
|
||||||
<FrameTitle>Опасная зона</FrameTitle>
|
|
||||||
<FrameDescription>Действия попадают в «Не сохранено» и в журнал</FrameDescription>
|
|
||||||
</FrameHeader>
|
|
||||||
<FramePanel className="p-0">
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
|
|
||||||
<div className="flex min-w-0 flex-col">
|
|
||||||
<span className="text-sm font-medium">{user.disabled ? 'Включить учётную запись' : 'Отключить учётную запись'}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">Вход закрывается, учётная запись и права сохраняются.</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() =>
|
|
||||||
patch([user.id], (u) => ({ ...u, disabled: !u.disabled }), `${user.disabled ? 'Включён' : 'Отключён'} ${user.email}`, (b, a) => [
|
|
||||||
`− disabled: ${String(b.disabled)}`,
|
|
||||||
`+ disabled: ${String(a.disabled)}`,
|
|
||||||
])
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{user.disabled ? 'Включить' : 'Отключить'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
|
|
||||||
<div className="flex min-w-0 flex-col">
|
|
||||||
<span className="text-sm font-medium">Удалить пользователя…</span>
|
|
||||||
<span className="text-muted-foreground text-xs">Учётная запись и все права будут удалены безвозвратно.</span>
|
|
||||||
</div>
|
|
||||||
<Button variant="destructive" onClick={() => setDeleteOpen(true)}>
|
|
||||||
Удалить…
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FramePanel>
|
|
||||||
</Frame>
|
|
||||||
</TabsContent>
|
|
||||||
</div>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<TypedConfirm
|
|
||||||
open={resetOpen}
|
|
||||||
onOpenChange={setResetOpen}
|
|
||||||
title="Сбросить passkeys пользователя?"
|
|
||||||
description={`Все ключи ${user.email} будут удалены. Войти можно будет только по паролю. Действие попадёт в журнал.`}
|
|
||||||
expected="сбросить"
|
|
||||||
confirmLabel="Сбросить"
|
|
||||||
onConfirm={() => {
|
|
||||||
patch([user.id], (u) => ({ ...u, passkey_count: 0 }), `Сброс passkeys: ${user.email}`, (b) => [`− passkeys: ${b.passkey_count}`, '+ passkeys: 0'])
|
|
||||||
setResetOpen(false)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TypedConfirm
|
|
||||||
open={deleteOpen}
|
|
||||||
onOpenChange={setDeleteOpen}
|
|
||||||
title="Удалить пользователя?"
|
|
||||||
description={`Учётная запись ${user.email} и её права будут удалены. Действие попадёт в панель «Не сохранено» и в журнал.`}
|
|
||||||
expected={user.email}
|
|
||||||
confirmLabel="Удалить"
|
|
||||||
onConfirm={() => {
|
|
||||||
addChange({
|
|
||||||
kind: 'del',
|
|
||||||
title: `Удалён ${user.email}`,
|
|
||||||
lines: [`− email: ${user.email}`, `− apps: ${user.apps.length}`],
|
|
||||||
resource: 'users',
|
|
||||||
marks: { [user.id]: 'deleted' },
|
|
||||||
commit: () => setUsers((prev) => prev.filter((u) => u.id !== user.id)),
|
|
||||||
})
|
|
||||||
setDeleteOpen(false)
|
|
||||||
void navigate({ to: '/design-preview/users' })
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { UsersIcon, ShieldCheckIcon, FingerprintIcon, UserXIcon } from 'lucide-react'
|
import { UsersIcon, ShieldCheckIcon, FingerprintIcon, UserXIcon } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import type { AdminUser, CreateUserRequest } from '@authportal/shared'
|
import type { AdminUser, CreateUserRequest } from '@authportal/shared'
|
||||||
import { CreateUserSheet } from '@/components/reui-kit/create-user-sheet'
|
import { CreateUserSheet } from '@/components/reui-kit/create-user-sheet'
|
||||||
@@ -26,7 +25,6 @@ export function UsersPage() {
|
|||||||
usersRef.current = users
|
usersRef.current = users
|
||||||
}, [users])
|
}, [users])
|
||||||
const patch = usePatchUsers()
|
const patch = usePatchUsers()
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const [createOpen, setCreateOpen] = useState(false)
|
const [createOpen, setCreateOpen] = useState(false)
|
||||||
const [journalFor, setJournalFor] = useState<AdminUser | null>(null)
|
const [journalFor, setJournalFor] = useState<AdminUser | null>(null)
|
||||||
@@ -86,7 +84,6 @@ export function UsersPage() {
|
|||||||
onToggleDisabled: onDeactivate,
|
onToggleDisabled: onDeactivate,
|
||||||
onResetPasskeys,
|
onResetPasskeys,
|
||||||
onDelete,
|
onDelete,
|
||||||
onOpenProfile: (u) => void navigate({ to: '/design-preview/users/$userId', params: { userId: u.id } }),
|
|
||||||
onBulkDelete: (ids) => setBulkDeleteIds(ids),
|
onBulkDelete: (ids) => setBulkDeleteIds(ids),
|
||||||
onBulkDisabled: (ids, disabled) =>
|
onBulkDisabled: (ids, disabled) =>
|
||||||
patch(ids, (u) => ({ ...u, disabled }), `${disabled ? 'Отключено' : 'Включено'} пользователей: ${ids.length}`, () => [
|
patch(ids, (u) => ({ ...u, disabled }), `${disabled ? 'Отключено' : 'Включено'} пользователей: ${ids.length}`, () => [
|
||||||
@@ -151,9 +148,6 @@ export function UsersPage() {
|
|||||||
key={accessFor.id}
|
key={accessFor.id}
|
||||||
user={accessFor}
|
user={accessFor}
|
||||||
onClose={() => setAccessFor(null)}
|
onClose={() => setAccessFor(null)}
|
||||||
onOpenProfile={() => {
|
|
||||||
void navigate({ to: '/design-preview/users/$userId', params: { userId: accessFor.id } })
|
|
||||||
}}
|
|
||||||
onSave={(value: AccessValue) => {
|
onSave={(value: AccessValue) => {
|
||||||
const target = accessFor
|
const target = accessFor
|
||||||
patch(
|
patch(
|
||||||
|
|||||||
@@ -31,10 +31,8 @@ import { Avatar, AvatarFallback } from '@authportal/ui/components/avatar'
|
|||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
|
||||||
BreadcrumbList,
|
BreadcrumbList,
|
||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
|
||||||
} from '@authportal/ui/components/breadcrumb'
|
} from '@authportal/ui/components/breadcrumb'
|
||||||
import { Button } from '@authportal/ui/components/button'
|
import { Button } from '@authportal/ui/components/button'
|
||||||
import {
|
import {
|
||||||
@@ -474,9 +472,7 @@ function FreshnessPopover() {
|
|||||||
|
|
||||||
export function PreviewHeader() {
|
export function PreviewHeader() {
|
||||||
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
const pathname = useRouterState({ select: (s) => s.location.pathname })
|
||||||
const { setPaletteOpen, users } = usePreviewStore()
|
const { setPaletteOpen } = usePreviewStore()
|
||||||
const profileId = /^\/design-preview\/users\/([^/]+)/.exec(pathname)?.[1]
|
|
||||||
const profileUser = profileId ? users.find((u) => u.id === profileId) : undefined
|
|
||||||
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
|
const current = NAV_ITEMS.find((i) => pathname === i.to || pathname.startsWith(`${i.to}/`))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -485,21 +481,9 @@ export function PreviewHeader() {
|
|||||||
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
<Separator orientation="vertical" className="mr-2 data-[orientation=vertical]:h-4" />
|
||||||
<Breadcrumb className="min-w-0">
|
<Breadcrumb className="min-w-0">
|
||||||
<BreadcrumbList className="flex-nowrap">
|
<BreadcrumbList className="flex-nowrap">
|
||||||
{profileUser ? (
|
<BreadcrumbItem className="min-w-0">
|
||||||
<>
|
<BreadcrumbPage className="truncate">{current?.label ?? 'Макет'}</BreadcrumbPage>
|
||||||
<BreadcrumbItem className="hidden @md/header:inline-flex">
|
</BreadcrumbItem>
|
||||||
<BreadcrumbLink render={<Link to="/design-preview/users" />}>Пользователи</BreadcrumbLink>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
<BreadcrumbSeparator className="hidden @md/header:block" />
|
|
||||||
<BreadcrumbItem className="min-w-0">
|
|
||||||
<BreadcrumbPage className="truncate">{profileUser.email}</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<BreadcrumbItem className="min-w-0">
|
|
||||||
<BreadcrumbPage className="truncate">{current?.label ?? 'Макет'}</BreadcrumbPage>
|
|
||||||
</BreadcrumbItem>
|
|
||||||
)}
|
|
||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
ScrollTextIcon,
|
ScrollTextIcon,
|
||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
UserRoundIcon,
|
|
||||||
FingerprintIcon,
|
FingerprintIcon,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { AdminUser } from '@authportal/shared'
|
import type { AdminUser } from '@authportal/shared'
|
||||||
@@ -43,7 +42,6 @@ function passkeyLabel(n: number) {
|
|||||||
|
|
||||||
export type UsersGridHandlers = {
|
export type UsersGridHandlers = {
|
||||||
onOpenAccess: (u: AdminUser) => void
|
onOpenAccess: (u: AdminUser) => void
|
||||||
onOpenProfile: (u: AdminUser) => void
|
|
||||||
onOpenJournal: (u: AdminUser) => void
|
onOpenJournal: (u: AdminUser) => void
|
||||||
onToggleDisabled: (u: AdminUser) => void
|
onToggleDisabled: (u: AdminUser) => void
|
||||||
onResetPasskeys: (u: AdminUser) => void
|
onResetPasskeys: (u: AdminUser) => void
|
||||||
@@ -91,10 +89,6 @@ export function UsersGrid({ users, handlers, marks }: { users: AdminUser[]; hand
|
|||||||
<ShieldCheckIcon aria-hidden />
|
<ShieldCheckIcon aria-hidden />
|
||||||
Права доступа
|
Права доступа
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => handlers.onOpenProfile(u)}>
|
|
||||||
<UserRoundIcon aria-hidden />
|
|
||||||
Профиль
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => handlers.onOpenJournal(u)}>
|
<DropdownMenuItem onClick={() => handlers.onOpenJournal(u)}>
|
||||||
<ScrollTextIcon aria-hidden />
|
<ScrollTextIcon aria-hidden />
|
||||||
Журнал
|
Журнал
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import { Route as DesignPreviewShellLoginsRouteImport } from './routes/design-pr
|
|||||||
import { Route as DesignPreviewShellOidcRouteImport } from './routes/design-preview._shell.oidc'
|
import { Route as DesignPreviewShellOidcRouteImport } from './routes/design-preview._shell.oidc'
|
||||||
import { Route as AuthAdminUsersUserIdRouteImport } from './routes/_auth.admin.users.$userId'
|
import { Route as AuthAdminUsersUserIdRouteImport } from './routes/_auth.admin.users.$userId'
|
||||||
import { Route as DesignPreviewShellUsersIndexRouteImport } from './routes/design-preview._shell.users.index'
|
import { Route as DesignPreviewShellUsersIndexRouteImport } from './routes/design-preview._shell.users.index'
|
||||||
import { Route as DesignPreviewShellUsersUserIdRouteImport } from './routes/design-preview._shell.users.$userId'
|
|
||||||
|
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
@@ -151,12 +150,6 @@ const DesignPreviewShellUsersIndexRoute =
|
|||||||
path: '/users/',
|
path: '/users/',
|
||||||
getParentRoute: () => DesignPreviewShellRoute,
|
getParentRoute: () => DesignPreviewShellRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const DesignPreviewShellUsersUserIdRoute =
|
|
||||||
DesignPreviewShellUsersUserIdRouteImport.update({
|
|
||||||
id: '/users/$userId',
|
|
||||||
path: '/users/$userId',
|
|
||||||
getParentRoute: () => DesignPreviewShellRoute,
|
|
||||||
} as any)
|
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
@@ -179,7 +172,6 @@ export interface FileRoutesByFullPath {
|
|||||||
'/design-preview/oidc': typeof DesignPreviewShellOidcRoute
|
'/design-preview/oidc': typeof DesignPreviewShellOidcRoute
|
||||||
'/admin/': typeof AuthAdminIndexRoute
|
'/admin/': typeof AuthAdminIndexRoute
|
||||||
'/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
'/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
||||||
'/design-preview/users/$userId': typeof DesignPreviewShellUsersUserIdRoute
|
|
||||||
'/design-preview/users/': typeof DesignPreviewShellUsersIndexRoute
|
'/design-preview/users/': typeof DesignPreviewShellUsersIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
@@ -201,7 +193,6 @@ export interface FileRoutesByTo {
|
|||||||
'/design-preview/oidc': typeof DesignPreviewShellOidcRoute
|
'/design-preview/oidc': typeof DesignPreviewShellOidcRoute
|
||||||
'/admin': typeof AuthAdminIndexRoute
|
'/admin': typeof AuthAdminIndexRoute
|
||||||
'/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
'/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
||||||
'/design-preview/users/$userId': typeof DesignPreviewShellUsersUserIdRoute
|
|
||||||
'/design-preview/users': typeof DesignPreviewShellUsersIndexRoute
|
'/design-preview/users': typeof DesignPreviewShellUsersIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
@@ -228,7 +219,6 @@ export interface FileRoutesById {
|
|||||||
'/design-preview/_shell/oidc': typeof DesignPreviewShellOidcRoute
|
'/design-preview/_shell/oidc': typeof DesignPreviewShellOidcRoute
|
||||||
'/_auth/admin/': typeof AuthAdminIndexRoute
|
'/_auth/admin/': typeof AuthAdminIndexRoute
|
||||||
'/_auth/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
'/_auth/admin/users/$userId': typeof AuthAdminUsersUserIdRoute
|
||||||
'/design-preview/_shell/users/$userId': typeof DesignPreviewShellUsersUserIdRoute
|
|
||||||
'/design-preview/_shell/users/': typeof DesignPreviewShellUsersIndexRoute
|
'/design-preview/_shell/users/': typeof DesignPreviewShellUsersIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
@@ -254,7 +244,6 @@ export interface FileRouteTypes {
|
|||||||
| '/design-preview/oidc'
|
| '/design-preview/oidc'
|
||||||
| '/admin/'
|
| '/admin/'
|
||||||
| '/admin/users/$userId'
|
| '/admin/users/$userId'
|
||||||
| '/design-preview/users/$userId'
|
|
||||||
| '/design-preview/users/'
|
| '/design-preview/users/'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
@@ -276,7 +265,6 @@ export interface FileRouteTypes {
|
|||||||
| '/design-preview/oidc'
|
| '/design-preview/oidc'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/admin/users/$userId'
|
| '/admin/users/$userId'
|
||||||
| '/design-preview/users/$userId'
|
|
||||||
| '/design-preview/users'
|
| '/design-preview/users'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
@@ -302,7 +290,6 @@ export interface FileRouteTypes {
|
|||||||
| '/design-preview/_shell/oidc'
|
| '/design-preview/_shell/oidc'
|
||||||
| '/_auth/admin/'
|
| '/_auth/admin/'
|
||||||
| '/_auth/admin/users/$userId'
|
| '/_auth/admin/users/$userId'
|
||||||
| '/design-preview/_shell/users/$userId'
|
|
||||||
| '/design-preview/_shell/users/'
|
| '/design-preview/_shell/users/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
@@ -476,13 +463,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DesignPreviewShellUsersIndexRouteImport
|
preLoaderRoute: typeof DesignPreviewShellUsersIndexRouteImport
|
||||||
parentRoute: typeof DesignPreviewShellRoute
|
parentRoute: typeof DesignPreviewShellRoute
|
||||||
}
|
}
|
||||||
'/design-preview/_shell/users/$userId': {
|
|
||||||
id: '/design-preview/_shell/users/$userId'
|
|
||||||
path: '/users/$userId'
|
|
||||||
fullPath: '/design-preview/users/$userId'
|
|
||||||
preLoaderRoute: typeof DesignPreviewShellUsersUserIdRouteImport
|
|
||||||
parentRoute: typeof DesignPreviewShellRoute
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +509,6 @@ interface DesignPreviewShellRouteChildren {
|
|||||||
DesignPreviewShellAuditRoute: typeof DesignPreviewShellAuditRoute
|
DesignPreviewShellAuditRoute: typeof DesignPreviewShellAuditRoute
|
||||||
DesignPreviewShellLoginsRoute: typeof DesignPreviewShellLoginsRoute
|
DesignPreviewShellLoginsRoute: typeof DesignPreviewShellLoginsRoute
|
||||||
DesignPreviewShellOidcRoute: typeof DesignPreviewShellOidcRoute
|
DesignPreviewShellOidcRoute: typeof DesignPreviewShellOidcRoute
|
||||||
DesignPreviewShellUsersUserIdRoute: typeof DesignPreviewShellUsersUserIdRoute
|
|
||||||
DesignPreviewShellUsersIndexRoute: typeof DesignPreviewShellUsersIndexRoute
|
DesignPreviewShellUsersIndexRoute: typeof DesignPreviewShellUsersIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,7 +519,6 @@ const DesignPreviewShellRouteChildren: DesignPreviewShellRouteChildren = {
|
|||||||
DesignPreviewShellAuditRoute: DesignPreviewShellAuditRoute,
|
DesignPreviewShellAuditRoute: DesignPreviewShellAuditRoute,
|
||||||
DesignPreviewShellLoginsRoute: DesignPreviewShellLoginsRoute,
|
DesignPreviewShellLoginsRoute: DesignPreviewShellLoginsRoute,
|
||||||
DesignPreviewShellOidcRoute: DesignPreviewShellOidcRoute,
|
DesignPreviewShellOidcRoute: DesignPreviewShellOidcRoute,
|
||||||
DesignPreviewShellUsersUserIdRoute: DesignPreviewShellUsersUserIdRoute,
|
|
||||||
DesignPreviewShellUsersIndexRoute: DesignPreviewShellUsersIndexRoute,
|
DesignPreviewShellUsersIndexRoute: DesignPreviewShellUsersIndexRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
import { UserProfilePage } from '@/design-preview/pages/user-profile-page'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/design-preview/_shell/users/$userId')({
|
|
||||||
component: UserProfilePage,
|
|
||||||
})
|
|
||||||
Reference in new issue
Block a user