fix(ui): выровнять chrome Frame и убрать Card-оболочки
Docker images / prepare-release (push) Successful in 4s
Docker images / backend-image (push) Failing after 2m36s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 55s
Docker images / publish-release (push) Skipped
Docker images / prepare-release (push) Successful in 4s
Docker images / backend-image (push) Failing after 2m36s
Docker images / frontend-image (push) Successful in 2m22s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 55s
Docker images / publish-release (push) Skipped
Подключить App Switcher, NavUser, OpsPanel и AlertDialog вместо Card-shell. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
+70
-101
@@ -5,9 +5,8 @@ import Link from "next/link"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { FormField, FormToggle } from "@/components/form-kit"
|
||||
import { FileImportDialog } from "@/components/file-import-dialog"
|
||||
import {
|
||||
Card, CardContent, CardHeader, CardTitle, CardDescription,
|
||||
} from "@/components/ui/card"
|
||||
import { OpsPanel } from "@/components/ops-panel"
|
||||
import { Frame, FramePanel } from "@/components/reui/frame"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
@@ -938,16 +937,15 @@ export default function SettingsPage() {
|
||||
<div className="space-y-4">
|
||||
|
||||
{/* ── Источник данных ── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Источник данных</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
{mockModeAvailable
|
||||
<OpsPanel
|
||||
title="Источник данных"
|
||||
description={
|
||||
mockModeAvailable
|
||||
? "Переключите между живыми данными от бекенда и суррогатными моками из lib/data.ts"
|
||||
: "В продакшен-образе доступны только живые данные от бекенда"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="divide-y px-5">
|
||||
: "В продакшен-образе доступны только живые данные от бекенда"
|
||||
}
|
||||
contentClassName="divide-y px-5"
|
||||
>
|
||||
|
||||
{/* mode toggle */}
|
||||
{mockModeAvailable ? (
|
||||
@@ -1042,13 +1040,10 @@ export default function SettingsPage() {
|
||||
<span>Бекенд недоступен — данные будут отображаться из кеша или моков. Запустите бекенд: <code className="font-mono bg-muted px-1 rounded">cd backend && npm run dev</code></span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</OpsPanel>
|
||||
|
||||
{/* ── Основные настройки ── */}
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Основные настройки</CardTitle></CardHeader>
|
||||
<CardContent className="divide-y px-5">
|
||||
<OpsPanel title="Основные настройки" contentClassName="divide-y px-5">
|
||||
<SettingRow label="Язык интерфейса">
|
||||
<select className="text-sm bg-background text-foreground border border-input rounded-md px-2 py-1 h-8 focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
value={lang} onChange={e => setLang(e.target.value)}>
|
||||
@@ -1083,18 +1078,13 @@ export default function SettingsPage() {
|
||||
description="Максимальное время ожидания при probe-тестах">
|
||||
<Input className="w-20 h-8 text-sm" value={probeTimeout} onChange={e => setProbeTimeout(e.target.value)} />
|
||||
</SettingRow>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</OpsPanel>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">База данных приложения</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Резервная копия SQLite бекенда: серверы, мониторинг, оповещения, EvoBGP. На время операции планировщик
|
||||
сбора данных приостанавливается.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="divide-y px-5">
|
||||
<OpsPanel
|
||||
title="База данных приложения"
|
||||
description="Резервная копия SQLite бекенда: серверы, мониторинг, оповещения, EvoBGP. На время операции планировщик сбора данных приостанавливается."
|
||||
contentClassName="divide-y px-5"
|
||||
>
|
||||
{!systemDbAvailable && (
|
||||
<div className="flex items-start gap-2 py-3 text-xs text-amber-600 dark:text-amber-400">
|
||||
<AlertCircleIcon className="size-3.5 mt-0.5 shrink-0" />
|
||||
@@ -1136,18 +1126,15 @@ export default function SettingsPage() {
|
||||
)}
|
||||
</div>
|
||||
</SettingRow>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</OpsPanel>
|
||||
|
||||
<Card id="route-ai" className="scroll-mt-4">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Route AI</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Параметры оптимизации маршрутов по умолчанию (тот же набор, что на странице «Оптимизатор маршрутов»). Пороги и
|
||||
веса настраиваются в инструменте, не через отдельный API.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 px-5 pb-5">
|
||||
<OpsPanel
|
||||
id="route-ai"
|
||||
className="scroll-mt-4"
|
||||
title="Route AI"
|
||||
description="Параметры оптимизации маршрутов по умолчанию (тот же набор, что на странице «Оптимизатор маршрутов»). Пороги и веса настраиваются в инструменте, не через отдельный API."
|
||||
contentClassName="flex flex-col gap-4 px-5 pb-5"
|
||||
>
|
||||
<dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-2 text-xs">
|
||||
<div className="flex justify-between gap-4 border-b border-border/50 py-1.5">
|
||||
<dt className="text-muted-foreground">Мин. выигрыш (переключение)</dt>
|
||||
@@ -1176,8 +1163,7 @@ export default function SettingsPage() {
|
||||
>
|
||||
Открыть оптимизатор маршрутов
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</OpsPanel>
|
||||
|
||||
</div>
|
||||
)
|
||||
@@ -1186,20 +1172,20 @@ export default function SettingsPage() {
|
||||
if (section === "EvoBGP") return (
|
||||
<div className="space-y-4">
|
||||
{(mode !== "live" || backendStatus !== true) && (
|
||||
<Card>
|
||||
<CardContent className="pt-4 pb-4 px-4">
|
||||
<Frame dense className="w-full">
|
||||
<FramePanel className="px-4 py-4">
|
||||
<p className="text-sm font-medium">Интеграция доступна в live-режиме</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Включите «Живые» данные и убедитесь, что локальный бекенд доступен (раздел «Общие»).
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">EvoBGP API</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
<OpsPanel
|
||||
title="EvoBGP API"
|
||||
description={
|
||||
<>
|
||||
Control plane EvoBGP: Bearer-ключ и роль viewer+ — см.{" "}
|
||||
<a
|
||||
className="underline underline-offset-2"
|
||||
@@ -1213,9 +1199,10 @@ export default function SettingsPage() {
|
||||
<code className="font-mono bg-muted px-1 rounded">evobgp_settings</code>
|
||||
). Каталог —{" "}
|
||||
<code className="font-mono bg-muted px-1 rounded">GET /v1/router-lists/catalog</code> через прокси.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="divide-y px-5 space-y-4 pb-5">
|
||||
</>
|
||||
}
|
||||
contentClassName="divide-y px-5 flex flex-col gap-4 pb-5"
|
||||
>
|
||||
<SettingRow
|
||||
label="Базовый URL API"
|
||||
description="Например http://control.example:8080 — без суффикса /v1"
|
||||
@@ -1361,17 +1348,14 @@ export default function SettingsPage() {
|
||||
{evo.error && (
|
||||
<p className="text-xs text-destructive">{evo.error}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</OpsPanel>
|
||||
</div>
|
||||
)
|
||||
|
||||
// ── Уведомления ──
|
||||
if (section === "Уведомления") return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Каналы уведомлений</CardTitle></CardHeader>
|
||||
<CardContent className="divide-y px-5">
|
||||
<OpsPanel title="Каналы уведомлений" contentClassName="divide-y px-5">
|
||||
<SettingRow label="Email" description="Отправка уведомлений на [email protected]">
|
||||
<FormToggle checked={notifEmail} onChange={setNotifEmail} />
|
||||
</SettingRow>
|
||||
@@ -1384,11 +1368,8 @@ export default function SettingsPage() {
|
||||
<FormToggle checked={notifWh} onChange={setNotifWh} />
|
||||
</SettingRow>
|
||||
{notifWh && <div className="py-3"><Input className="text-sm h-8 font-mono" defaultValue="https://hooks.example.com/routerlists" /></div>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Триггеры</CardTitle></CardHeader>
|
||||
<CardContent className="divide-y px-5">
|
||||
</OpsPanel>
|
||||
<OpsPanel title="Триггеры" contentClassName="divide-y px-5">
|
||||
<SettingRow label="Деградация узла" description="Потери пакетов > 5% или RTT > 100мс">
|
||||
<FormToggle checked={notifDegr} onChange={setNotifDegr} />
|
||||
</SettingRow>
|
||||
@@ -1401,8 +1382,7 @@ export default function SettingsPage() {
|
||||
<SettingRow label="Просроченный бэкап" description="Если последний бэкап старше 2 дней">
|
||||
<FormToggle checked={notifBackup} onChange={setNotifBackup} />
|
||||
</SettingRow>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</OpsPanel>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1419,7 +1399,8 @@ export default function SettingsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card className="overflow-hidden gap-0 py-0">
|
||||
<Frame dense className="w-full">
|
||||
<FramePanel className="overflow-hidden p-0">
|
||||
{/* table header */}
|
||||
<div className="grid grid-cols-[1fr_120px_100px_80px_auto] items-center gap-3 px-4 py-2 border-b bg-muted/30 text-[11px] font-medium text-muted-foreground">
|
||||
<span>Пользователь</span>
|
||||
@@ -1475,7 +1456,8 @@ export default function SettingsPage() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
|
||||
{/* access summary */}
|
||||
<DataPageCard>
|
||||
@@ -1500,8 +1482,8 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{apiKeys.map(k => (
|
||||
<Card key={k.id}>
|
||||
<CardContent className="pt-4 pb-3 px-4">
|
||||
<Frame key={k.id} dense className="w-full">
|
||||
<FramePanel className="pt-4 pb-3 px-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">{k.name}</p>
|
||||
@@ -1529,16 +1511,15 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
))}
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Документация API</CardTitle>
|
||||
<CardDescription className="text-xs">Base URL: https://api.routerlists.io/v1</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4">
|
||||
<OpsPanel
|
||||
title="Документация API"
|
||||
description="Base URL: https://api.routerlists.io/v1"
|
||||
contentClassName="px-4 pb-4"
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{[
|
||||
["GET", "/servers", "Список серверов"],
|
||||
@@ -1556,17 +1537,14 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</OpsPanel>
|
||||
</div>
|
||||
)
|
||||
|
||||
// ── Безопасность ──
|
||||
if (section === "Безопасность") return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Аутентификация</CardTitle></CardHeader>
|
||||
<CardContent className="divide-y px-5">
|
||||
<OpsPanel title="Аутентификация" contentClassName="divide-y px-5">
|
||||
<SettingRow label="Двухфакторная аутентификация (MFA)"
|
||||
description="TOTP / Authenticator app для всех администраторов">
|
||||
<FormToggle checked={mfa} onChange={setMfa} />
|
||||
@@ -1574,30 +1552,22 @@ export default function SettingsPage() {
|
||||
<SettingRow label="Тайм-аут сессии (мин)" description="Автоматический выход при бездействии">
|
||||
<Input className="w-20 h-8 text-sm" value={sessMin} onChange={e => setSessMin(e.target.value)} />
|
||||
</SettingRow>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Allowlist IP-адресов</CardTitle>
|
||||
<CardDescription className="text-xs">Доступ разрешён только с этих сетей. Одна запись на строку.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 pb-4">
|
||||
</OpsPanel>
|
||||
<OpsPanel
|
||||
title="Allowlist IP-адресов"
|
||||
description="Доступ разрешён только с этих сетей. Одна запись на строку."
|
||||
contentClassName="px-4 pb-4"
|
||||
>
|
||||
<textarea value={ipAllow} onChange={e => setIpAllow(e.target.value)} rows={4}
|
||||
className="w-full rounded-md border bg-muted px-3 py-2 text-xs font-mono resize-none outline-none focus:ring-1 focus:ring-ring" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base">Аудит</CardTitle></CardHeader>
|
||||
<CardContent className="divide-y px-5">
|
||||
</OpsPanel>
|
||||
<OpsPanel title="Аудит" contentClassName="divide-y px-5">
|
||||
<SettingRow label="Расширенный журнал аудита"
|
||||
description="Записывать все изменения конфигурации с указанием пользователя и IP">
|
||||
<FormToggle checked={auditLog} onChange={setAuditLog} />
|
||||
</SettingRow>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader><CardTitle className="text-base text-destructive">Опасная зона</CardTitle></CardHeader>
|
||||
<CardContent className="px-4 pb-4">
|
||||
</OpsPanel>
|
||||
<OpsPanel title={<span className="text-destructive">Опасная зона</span>} contentClassName="px-4 pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Сбросить все настройки</p>
|
||||
@@ -1607,8 +1577,7 @@ export default function SettingsPage() {
|
||||
Сбросить
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</OpsPanel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user