fix(ui): выровнять confirms и токены под ReUI PRO
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 1m31s
Docker images / frontend-image (push) Successful in 2m11s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 40s
Docker images / publish-release (push) Successful in 10s
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 1m31s
Docker images / frontend-image (push) Successful in 2m11s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 40s
Docker images / publish-release (push) Successful in 10s
Добавить ui-design-contract, AlertDialog вместо hand-roll, CodeExportSheet для preview фильтров, ReUI Badge и gap вместо space-y. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -16,6 +16,17 @@ import { Separator } from "@/components/ui/separator"
|
||||
import {
|
||||
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter,
|
||||
} from "@/components/ui/sheet"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Flag } from "@/components/flag"
|
||||
import { StatusDot } from "@/components/status-dot"
|
||||
import { servers } from "@/lib/data"
|
||||
@@ -152,7 +163,7 @@ const PERM_OPTS: { v: PermLevel; label: string }[] = [
|
||||
const PERM_COLOR: Record<PermLevel, string> = {
|
||||
none: "bg-muted text-muted-foreground",
|
||||
read: "bg-sky-500/10 text-sky-600 dark:text-sky-400",
|
||||
write: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400",
|
||||
write: "bg-success/10 text-success",
|
||||
}
|
||||
|
||||
const SECTIONS_NAV = ["Общие", "EvoBGP", "Уведомления", "Пользователи", "API-ключи", "Безопасность"] as const
|
||||
@@ -188,8 +199,8 @@ function PermPills({ value, onChange, disabled }: { value: PermLevel; onChange:
|
||||
"px-2 text-[11px] font-medium transition-colors",
|
||||
i < PERM_OPTS.length - 1 && "border-r border-input",
|
||||
value === o.v
|
||||
? o.v === "write" ? "bg-emerald-600 text-white dark:bg-emerald-500"
|
||||
: o.v === "read" ? "bg-sky-600 text-white dark:bg-sky-500"
|
||||
? o.v === "write" ? "bg-success text-white"
|
||||
: o.v === "read" ? "bg-info text-white"
|
||||
: "bg-muted-foreground/70 text-white"
|
||||
: "text-muted-foreground hover:bg-muted",
|
||||
)}
|
||||
@@ -685,68 +696,79 @@ function UserSheet({ open, user, onSave, onClose }: {
|
||||
// ─── delete confirm ───────────────────────────────────────────────────────────
|
||||
|
||||
function DatabaseRestoreConfirm({
|
||||
open,
|
||||
filename,
|
||||
busy,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
open: boolean
|
||||
filename: string
|
||||
busy?: boolean
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onCancel} />
|
||||
<div className="relative z-10 w-full max-w-sm mx-4 bg-card rounded-xl border shadow-2xl p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-9 rounded-full bg-destructive/10 flex items-center justify-center shrink-0">
|
||||
<AlertCircleIcon className="size-4 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">Восстановить базу приложения?</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5 break-all">{filename}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Текущие данные SQLite на бекенде будут полностью заменены содержимым файла. Рекомендуется сначала скачать
|
||||
актуальный бэкап.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1" onClick={onCancel} disabled={busy}>Отмена</Button>
|
||||
<Button variant="destructive" className="flex-1" onClick={onConfirm} disabled={busy}>
|
||||
<AlertDialog open={open} onOpenChange={(v) => { if (!v && !busy) onCancel() }}>
|
||||
<AlertDialogContent size="default">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia className="bg-destructive/10 text-destructive">
|
||||
<AlertCircleIcon />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Восстановить базу приложения?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Файл: {filename}. Текущие данные SQLite на бекенде будут полностью заменены.
|
||||
Рекомендуется сначала скачать актуальный бэкап.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={busy} onClick={onCancel}>Отмена</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
variant="destructive"
|
||||
disabled={busy}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{busy ? <LoaderCircleIcon className="size-4 animate-spin" /> : null}
|
||||
{busy ? "Восстановление…" : "Восстановить"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
function DeleteConfirm({ user, onConfirm, onCancel }: { user: User; onConfirm: () => void; onCancel: () => void }) {
|
||||
function DeleteConfirm({
|
||||
open,
|
||||
user,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
open: boolean
|
||||
user: User
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onCancel} />
|
||||
<div className="relative z-10 w-full max-w-sm mx-4 bg-card rounded-xl border shadow-2xl p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-9 rounded-full bg-destructive/10 flex items-center justify-center shrink-0">
|
||||
<TrashIcon className="size-4 text-destructive" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold">Удалить пользователя?</p>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">{user.name} · @{user.login}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Это действие нельзя отменить. Пользователь потеряет доступ немедленно.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1" onClick={onCancel}>Отмена</Button>
|
||||
<Button variant="destructive" className="flex-1" onClick={onConfirm}>Удалить</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AlertDialog open={open} onOpenChange={(v) => { if (!v) onCancel() }}>
|
||||
<AlertDialogContent size="default">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia className="bg-destructive/10 text-destructive">
|
||||
<TrashIcon />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Удалить пользователя?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{user.name} · @{user.login}. Это действие нельзя отменить.
|
||||
Пользователь потеряет доступ немедленно.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>Отмена</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive" onClick={onConfirm}>
|
||||
Удалить
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -937,7 +959,7 @@ export default function SettingsPage() {
|
||||
|
||||
// ── Общие ──
|
||||
if (section === "Общие") return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
{/* ── Источник данных ── */}
|
||||
<OpsPanel
|
||||
@@ -960,9 +982,7 @@ export default function SettingsPage() {
|
||||
"px-3 text-xs transition-colors border-input",
|
||||
i === 0 && "border-r",
|
||||
mode === v
|
||||
? v === "live"
|
||||
? "bg-emerald-600 text-white dark:bg-emerald-500"
|
||||
: "bg-primary text-primary-foreground"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-muted",
|
||||
)}>
|
||||
{l}
|
||||
@@ -972,7 +992,7 @@ export default function SettingsPage() {
|
||||
</SettingRow>
|
||||
) : (
|
||||
<SettingRow label="Режим данных" description="В Docker-образе приложение работает только с живыми данными бекенда">
|
||||
<span className="inline-flex h-8 items-center rounded-md border border-input bg-emerald-600 px-3 text-xs text-white dark:bg-emerald-500">
|
||||
<span className="inline-flex h-8 items-center rounded-md border border-input bg-primary px-3 text-xs text-primary-foreground">
|
||||
Живые
|
||||
</span>
|
||||
</SettingRow>
|
||||
@@ -1173,7 +1193,7 @@ export default function SettingsPage() {
|
||||
|
||||
// ── EvoBGP ──
|
||||
if (section === "EvoBGP") return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
{(mode !== "live" || backendStatus !== true) && (
|
||||
<Frame dense className="w-full">
|
||||
<FramePanel className="px-4 py-4">
|
||||
@@ -1357,7 +1377,7 @@ export default function SettingsPage() {
|
||||
|
||||
// ── Уведомления ──
|
||||
if (section === "Уведомления") return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<OpsPanel title="Каналы уведомлений" contentClassName="divide-y px-5">
|
||||
<SettingRow label="Email" description="Отправка уведомлений на [email protected]">
|
||||
<FormToggle checked={notifEmail} onChange={setNotifEmail} />
|
||||
@@ -1479,11 +1499,11 @@ export default function SettingsPage() {
|
||||
|
||||
// ── API-ключи ──
|
||||
if (section === "API-ключи") return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-end">
|
||||
<Button size="sm"><PlusIcon className="size-4" />Создать ключ</Button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
{apiKeys.map(k => (
|
||||
<Frame key={k.id} dense className="w-full">
|
||||
<FramePanel className="pt-4 pb-3 px-4">
|
||||
@@ -1546,7 +1566,7 @@ export default function SettingsPage() {
|
||||
|
||||
// ── Безопасность ──
|
||||
if (section === "Безопасность") return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<OpsPanel title="Аутентификация" contentClassName="divide-y px-5">
|
||||
<SettingRow label="Двухфакторная аутентификация (MFA)"
|
||||
description="TOTP / Authenticator app для всех администраторов">
|
||||
@@ -1647,6 +1667,7 @@ export default function SettingsPage() {
|
||||
{/* delete confirm */}
|
||||
{dbRestoreFile && (
|
||||
<DatabaseRestoreConfirm
|
||||
open={!!dbRestoreFile}
|
||||
filename={dbRestoreFile.name}
|
||||
busy={dbRestoreBusy}
|
||||
onConfirm={() => { void handleSystemDatabaseRestoreConfirm() }}
|
||||
@@ -1658,6 +1679,7 @@ export default function SettingsPage() {
|
||||
)}
|
||||
{deleteTarget && (
|
||||
<DeleteConfirm
|
||||
open={!!deleteTarget}
|
||||
user={deleteTarget}
|
||||
onConfirm={() => { setUsers(p => p.filter(u => u.id !== deleteTarget.id)); setDeleteTarget(null) }}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
|
||||
Reference in New Issue
Block a user