fix(ui): заменить таблицы на карточки данных и улучшить функциональность поиска
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
Docker images / prepare-release (push) Successful in 6s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 1m50s
Docker images / updater-image (push) Successful in 44s
Docker images / notify-webhook (push) Has been skipped
Docker images / publish-release (push) Successful in 7s
This commit is contained in:
+18
-150
@@ -26,6 +26,9 @@ import {
|
||||
ServerIcon, LayoutDashboardIcon, RefreshCwIcon, CableIcon, LoaderCircleIcon,
|
||||
DownloadIcon, UploadIcon,
|
||||
} from "lucide-react"
|
||||
import { SubusersDataGrid } from "@/components/data-grids/subusers-data-grid"
|
||||
import { SettingsAccessSummaryDataGrid } from "@/components/data-grids/settings-access-summary-data-grid"
|
||||
import { DataPageCard } from "@/components/data-page-card"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { requestJson } from "@/shared/api/http-client"
|
||||
import { downloadSystemDatabaseBackup, restoreSystemDatabaseBackup } from "@/shared/api/system-database"
|
||||
@@ -568,100 +571,14 @@ function UserSheet({ open, user, onSave, onClose }: {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* table header */}
|
||||
{form.subUsers.length > 0 && (
|
||||
<div className="grid items-center gap-2 px-4 py-1.5 bg-muted/20 border-b text-[10px] font-semibold uppercase tracking-widest text-muted-foreground"
|
||||
style={{ gridTemplateColumns: "1fr 130px 120px 90px 36px 32px" }}>
|
||||
<span>Логин / описание</span>
|
||||
<span>Пароль</span>
|
||||
<span>JH-серверы</span>
|
||||
<span>IP-клиента</span>
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* rows */}
|
||||
<div className="divide-y divide-border/60">
|
||||
{form.subUsers.length === 0 && !addSubOpen && (
|
||||
<div className="flex flex-col items-center justify-center py-12 gap-2 text-muted-foreground">
|
||||
<CableIcon className="size-6 opacity-20" />
|
||||
<p className="text-sm">Нет GRE-клиентов</p>
|
||||
<p className="text-xs opacity-60">Добавьте учётки для подключения устройств</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{form.subUsers.map(su => {
|
||||
const jhs = servers.filter(s => su.jhServerIds.includes(s.id))
|
||||
const revealed = revealedIds.has(su.id)
|
||||
return (
|
||||
<div key={su.id}
|
||||
className={cn(
|
||||
"grid items-center gap-2 px-4 py-2.5 hover:bg-muted/20 transition-colors",
|
||||
!su.active && "opacity-50",
|
||||
)}
|
||||
style={{ gridTemplateColumns: "1fr 130px 120px 90px 36px 32px" }}>
|
||||
|
||||
{/* login + description */}
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-mono font-medium truncate">{su.login}</p>
|
||||
{su.description && (
|
||||
<p className="text-[11px] text-muted-foreground truncate">{su.description}</p>
|
||||
)}
|
||||
{su.lastSeen && (
|
||||
<p className="text-[10px] text-muted-foreground/50">{su.lastSeen}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* password */}
|
||||
<div className="flex items-center gap-1 min-w-0">
|
||||
<span className="font-mono text-[11px] truncate flex-1">
|
||||
{revealed ? su.password : "••••••••••••"}
|
||||
</span>
|
||||
<button onClick={() => toggleReveal(su.id)}
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground shrink-0 transition-colors">
|
||||
{revealed
|
||||
? <EyeOffIcon className="size-3" />
|
||||
: <EyeIcon className="size-3" />}
|
||||
</button>
|
||||
<button onClick={() => navigator.clipboard.writeText(su.password).catch(() => {})}
|
||||
className="text-muted-foreground/50 hover:text-muted-foreground shrink-0 transition-colors">
|
||||
<CopyIcon className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* JH servers */}
|
||||
<div className="flex flex-wrap gap-1 min-w-0">
|
||||
{jhs.length === 0
|
||||
? <span className="text-[11px] text-muted-foreground/40">—</span>
|
||||
: jhs.map(jh => (
|
||||
<span key={jh.id} className="inline-flex items-center gap-1 text-[10px] font-medium
|
||||
bg-violet-500/10 text-violet-600 dark:text-violet-400 border border-violet-500/20
|
||||
rounded px-1 py-0.5">
|
||||
<Flag code={jh.country} size={10} />
|
||||
{jh.name.split("-").slice(-1)[0]}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* client IP */}
|
||||
<span className="font-mono text-[11px] text-muted-foreground truncate">
|
||||
{su.clientIp || "—"}
|
||||
</span>
|
||||
|
||||
{/* active toggle */}
|
||||
<FormToggle checked={su.active} onChange={() => toggleSubUser(su.id)} />
|
||||
|
||||
{/* delete */}
|
||||
<button onClick={() => removeSubUser(su.id)}
|
||||
className="text-muted-foreground/40 hover:text-destructive transition-colors flex justify-end">
|
||||
<TrashIcon className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<SubusersDataGrid
|
||||
subUsers={form.subUsers}
|
||||
servers={servers}
|
||||
revealedIds={revealedIds}
|
||||
onToggleReveal={toggleReveal}
|
||||
onToggleActive={toggleSubUser}
|
||||
onRemove={removeSubUser}
|
||||
/>
|
||||
|
||||
{/* inline add form */}
|
||||
{addSubOpen ? (
|
||||
@@ -1561,66 +1478,17 @@ export default function SettingsPage() {
|
||||
</Card>
|
||||
|
||||
{/* access summary */}
|
||||
<Card className="overflow-hidden gap-0 py-0">
|
||||
<DataPageCard>
|
||||
<div className="flex items-center gap-3 px-4 py-3 border-b">
|
||||
<UserIcon className="size-4 text-muted-foreground shrink-0" />
|
||||
<span className="text-sm font-medium">Сводка прав доступа</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-[11px] text-muted-foreground bg-muted/30">
|
||||
<th className="text-left font-medium px-4 py-2">Пользователь</th>
|
||||
<th className="text-left font-medium px-4 py-2">Разделы</th>
|
||||
<th className="text-left font-medium px-4 py-2">Серверы</th>
|
||||
<th className="text-left font-medium px-4 py-2">Права записи</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border/60">
|
||||
{users.map(u => {
|
||||
const writeSections = u.role === "admin" ? ALL_SECTIONS : u.sections.filter(s => s.level === "write").map(s => s.section)
|
||||
const readSections = u.role === "admin" ? [] : u.sections.filter(s => s.level === "read").map(s => s.section)
|
||||
const accessServers = u.role === "admin" ? servers : servers.filter(s => u.servers.find(p => p.serverId === s.id && p.level !== "none"))
|
||||
return (
|
||||
<tr key={u.id} className="hover:bg-muted/20 transition-colors">
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<AvatarCircle avatar={u.avatar} active={u.active} />
|
||||
<span className="text-sm font-medium">{u.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-xs text-muted-foreground">
|
||||
{u.role === "admin"
|
||||
? <span className="text-violet-600 dark:text-violet-400 font-medium">Все ({ALL_SECTIONS.length})</span>
|
||||
: <span>{(readSections.length + writeSections.length)} из {ALL_SECTIONS.length}</span>}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-xs text-muted-foreground">
|
||||
{u.role === "admin"
|
||||
? <span className="text-violet-600 dark:text-violet-400 font-medium">Все ({servers.length})</span>
|
||||
: <span>{accessServers.length} из {servers.length}</span>}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{u.role === "admin"
|
||||
? <span className="text-[10px] px-1.5 py-0.5 rounded bg-violet-500/10 text-violet-600 dark:text-violet-400 border border-violet-500/20">Полный доступ</span>
|
||||
: writeSections.length === 0
|
||||
? <span className="text-[10px] text-muted-foreground">Только просмотр</span>
|
||||
: writeSections.slice(0, 3).map(s => (
|
||||
<span key={s} className="text-[10px] px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20">{s}</span>
|
||||
))
|
||||
}
|
||||
{u.role !== "admin" && writeSections.length > 3 && (
|
||||
<span className="text-[10px] text-muted-foreground">+{writeSections.length - 3}</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
<SettingsAccessSummaryDataGrid
|
||||
users={users}
|
||||
servers={servers}
|
||||
allSectionsCount={ALL_SECTIONS.length}
|
||||
/>
|
||||
</DataPageCard>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user