diff --git a/.ci/scripts/patch-servers-reui.py b/.ci/scripts/patch-servers-reui.py new file mode 100644 index 0000000..6672da2 --- /dev/null +++ b/.ci/scripts/patch-servers-reui.py @@ -0,0 +1,144 @@ +import re +from pathlib import Path + +path = Path(__file__).resolve().parents[2] / "app" / "(main)" / "servers" / "page.tsx" +text = path.read_text(encoding="utf-8") + +text = text.replace( + 'import { Fragment, useEffect, useMemo, useState } from "react"\n' + 'import { PageHeader } from "@/components/page-header"\n' + 'import { StatusBadge } from "@/components/status-badge"', + 'import { useEffect, useMemo, useState } from "react"\n' + 'import { PageHeader } from "@/components/page-header"\n' + 'import { FormField, FormToggle, SectionTitle, SegmentedControl } from "@/components/form-kit"\n' + 'import { DataPageToolbar } from "@/components/data-page-toolbar"\n' + 'import { ServersDataGrid } from "@/components/data-grids/servers-data-grid"', + 1, +) + +text = text.replace( + """import { + DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, + DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel, DropdownMenuGroup, +} from "@/components/ui/dropdown-menu" +""", + """import { + Stepper, + StepperContent, + StepperIndicator, + StepperItem, + StepperNav, + StepperPanel, + StepperSeparator, + StepperTitle, + StepperTrigger, +} from "@/components/reui/stepper" +""", + 1, +) + +text = text.replace( + """import { + SearchIcon, RefreshCwIcon, DownloadIcon, PlusIcon, TrashIcon, + MoreHorizontalIcon, EyeIcon, EyeOffIcon, + ChevronRightIcon, ChevronDownIcon, + CheckCircleIcon, XCircleIcon, LoaderCircleIcon, + ShieldIcon, WifiIcon, PencilIcon, PowerIcon, Trash2Icon, ExternalLinkIcon, + HomeIcon, ServerIcon, NetworkIcon, +} from "lucide-react\"""", + """import { + RefreshCwIcon, DownloadIcon, PlusIcon, TrashIcon, + EyeIcon, EyeOffIcon, + ChevronRightIcon, ChevronDownIcon, + CheckCircleIcon, XCircleIcon, LoaderCircleIcon, + ShieldIcon, WifiIcon, + HomeIcon, ServerIcon, NetworkIcon, +} from "lucide-react\"""", + 1, +) + +text = re.sub( + r"// ─── RouterOS version utilities.*?// ─── Countries ─", + "// ─── Countries ─", + text, + count=1, + flags=re.S, +) + +text = re.sub( + r"// ─── Type config ─.*?// ─── Shared small components ─", + "// ─── Shared small components ─", + text, + count=1, + flags=re.S, +) + +text = re.sub( + r"function Field\(\{ label, hint, required, children \}:.*?^}\n\n// ─── Country field", + "// ─── Country field", + text, + count=1, + flags=re.S | re.M, +) + +text = text.replace( + "const [expandedId, setExpandedId] = useState(null)", + "const [sheetStep, setSheetStep] = useState(1)", + 1, +) + +text = text.replace( + 'setForm(defaultForm); setTestState("idle"); setTestMsg("")\n setOpen(true)', + 'setForm(defaultForm); setTestState("idle"); setTestMsg("")\n setSheetStep(1)\n setOpen(true)', + 1, +) + +text = text.replace( + 'setTestState("idle"); setTestMsg(""); setOpen(true)', + 'setTestState("idle"); setTestMsg(""); setSheetStep(1); setOpen(true)', + 1, +) + +text = re.sub(r"", "", text) +text = re.sub(r" + ({ + value: tab.value, + label: tab.label, + count: tab.value === "all" ? counts.all : counts[tab.value as ServerType], + })), + }} + search={search} + onSearchChange={setSearch} + searchPlaceholder="Поиск по имени, хосту…" + countLabel={`${filtered.length} серверов`} + /> + + +""" + +text = re.sub( + r" \{/\* Table \*/\}\n .*?\n", + new_table, + text, + count=1, + flags=re.S, +) + +path.write_text(text, encoding="utf-8") +print("patched", path) diff --git a/.ci/scripts/patch-servers-stepper.py b/.ci/scripts/patch-servers-stepper.py new file mode 100644 index 0000000..7d9fc9f --- /dev/null +++ b/.ci/scripts/patch-servers-stepper.py @@ -0,0 +1,144 @@ +import re +from pathlib import Path + +path = Path(__file__).resolve().parents[2] / "app" / "(main)" / "servers" / "page.tsx" +text = path.read_text(encoding="utf-8") + +# Wrap sheet form in stepper +text = text.replace( + """
+ + {/* 1. Основные */} +
+ Основные""", + """ + + + + 1 + Основные + + + + + + 2 + WAN + + + + + + 3 + API + + + + + + 4 + Дополнительно + + + + + """, + 1, +) + +text = text.replace( + """
+ + {/* 2. WAN-аплинки (только для home-router) */} + {isHomeRouter && ( +
+ WAN-аплинки""", + """ + + WAN-аплинки""", + 1, +) + +text = text.replace( + """
+ )} + + {/* 3. Подключение (API) */} +
+ Подключение (RouterOS REST API)""", + """ + + Подключение (RouterOS REST API)""", + 1, +) + +text = text.replace( + """
+ + {/* 4. Дополнительно */} +
+
+ +
+ + + }>Отмена + + """, + """ + + + + + }>Отмена + {sheetStep > 1 && ( + + )} + {sheetStep < 4 ? ( + + ) : ( + + )} + """, + 1, +) + +# WAN step 2: show message when not home router +text = text.replace( + """ + WAN-аплинки + + WAN-аплинки + {!isHomeRouter ? ( +

WAN-аплинки доступны только для типа Home Router.

+ ) : ( + +
+ + )} + + void; disabled?: boolean }) { - return ( - - ) -} - function FieldLabel({ children, className }: { children: React.ReactNode; className?: string }) { return

{children}

} @@ -739,7 +723,7 @@ function AlertRuleRow({ rule, onToggle, onDelete, onEdit, interactionsDisabled } !rule.enabled && "opacity-55", )}> {/* toggle */} - onToggle(rule.id, v)} disabled={interactionsDisabled} /> + onToggle(rule.id, v)} disabled={interactionsDisabled} /> {/* severity dot */} diff --git a/app/(main)/asns/page.tsx b/app/(main)/asns/page.tsx index 27c0dc1..cbb7833 100644 --- a/app/(main)/asns/page.tsx +++ b/app/(main)/asns/page.tsx @@ -1,18 +1,21 @@ "use client" -import { useMemo } from "react" +import { useMemo, useState } from "react" import { PageHeader } from "@/components/page-header" import { DataTable } from "@/components/data-table" +import { FileImportDialog } from "@/components/file-import-dialog" import { asns as mockAsns } from "@/lib/data" import { Button } from "@/components/ui/button" import { UploadIcon, DownloadIcon, PlusIcon, FilterIcon, LoaderCircleIcon } from "lucide-react" import { useDataSource } from "@/lib/data-source" import { useEvoBGP } from "@/lib/evobgp-context" import { cn } from "@/lib/utils" +import { toast } from "sonner" export default function AsnsPage() { const { mode } = useDataSource() const { enabled, snapshot, loading, error } = useEvoBGP() + const [importOpen, setImportOpen] = useState(false) const useEvoCatalog = mode === "live" && enabled @@ -28,7 +31,9 @@ export default function AsnsPage() { crumbs={[{ label: "Данные" }, { label: "ASN" }]} actions={ <> - + @@ -57,6 +62,7 @@ export default function AsnsPage() { + { + toast.info(`Выбран файл: ${files[0]?.name ?? "—"}`) + }} + /> ) } diff --git a/app/(main)/backups/page.tsx b/app/(main)/backups/page.tsx index bb33ef3..a5b3393 100644 --- a/app/(main)/backups/page.tsx +++ b/app/(main)/backups/page.tsx @@ -2,6 +2,10 @@ import { useCallback, useEffect, useMemo, useState } from "react" import { PageHeader } from "@/components/page-header" +import { DataPageToolbar } from "@/components/data-page-toolbar" +import { BackupsDataGrid } from "@/components/data-grids/backups-data-grid" +import { FileImportDialog } from "@/components/file-import-dialog" +import { FormField, FormToggle, SectionTitle, SegmentedControl } from "@/components/form-kit" import { StatusBadge } from "@/components/status-badge" import type { Backup, Server } from "@/lib/data" import { Card, CardContent } from "@/components/ui/card" @@ -22,52 +26,17 @@ import { listServers } from "@/shared/api/servers" import { toFrontendServer } from "@/entities/server/model/mappers" import { createBackupsAsync, deleteBackup, getBackupJob, getBackupScheduleSettings, listBackups, putBackupScheduleSettings, type BackupItem } from "@/shared/api/backups" import { toast } from "sonner" - -// ─── small UI helpers ───────────────────────────────────────────────────────── - -function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { - return ( - - ) -} - -function SectionTitle({ icon, children }: { icon: React.ReactNode; children: React.ReactNode }) { - return ( -
- {icon} - {children} -
-
- ) -} - -function Field({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { - return ( -
- - {children} - {hint &&

{hint}

} -
- ) -} - -function SegmentedControl({ value, onChange, options }: { - value: T; onChange: (v: T) => void; options: { value: T; label: string }[] -}) { - return ( -
- {options.map((o) => ( - - ))} -
- ) -} +import { + Stepper, + StepperContent, + StepperIndicator, + StepperItem, + StepperNav, + StepperPanel, + StepperSeparator, + StepperTitle, + StepperTrigger, +} from "@/components/reui/stepper" // ─── types ──────────────────────────────────────────────────────────────────── @@ -168,8 +137,11 @@ export default function BackupsPage() { // Manual backup sheet const [manualOpen, setManualOpen] = useState(false) + const [manualStep, setManualStep] = useState(1) const [manualServers, setManualServers] = useState>(new Set()) const [manualNotes, setManualNotes] = useState("") + const [restoreOpen, setRestoreOpen] = useState(false) + const [restoreTarget, setRestoreTarget] = useState(null) function toggleManualServer(id: string) { setManualServers((prev) => { const next = new Set(prev) @@ -356,7 +328,7 @@ export default function BackupsPage() { - ))} -
- {filtered.length} бэкапов - - -
- - - - - - - - - - - - - {filtered.length === 0 && ( - - )} - {filtered.map((b) => ( - - - - - - - - - - ))} - -
ФайлСерверРазмерТипЗаметкиСоздан -
Нет бэкапов
{b.filename}{b.server}{b.size} - - {b.kind === "auto" ? "авто" : "вручную"} - - {b.notes || "—"}{b.created} -
- - - -
-
-
+ + { + setRestoreTarget(b) + setRestoreOpen(true) + }} + onDelete={handleDelete} + /> )} @@ -517,11 +435,11 @@ export default function BackupsPage() {

Автоматический бэкап

Создавать бэкапы по расписанию

- setSched("enabled", v)} /> + setSched("enabled", v)} />
- + setSched("frequency", v)} @@ -531,10 +449,10 @@ export default function BackupsPage() { { value: "monthly", label: "Ежемесячно" }, ]} /> - + {schedule.frequency === "weekly" && ( - +
{WEEK_DAYS.map((d, i) => ( ))}
-
+ )} {schedule.frequency === "monthly" && ( - + setSched("monthDay", Math.min(28, Math.max(1, Number(e.target.value))))} /> - + )} - +
- +
- + setSched("keepCount", Math.max(1, Number(e.target.value)))} /> - - + + setSched("format", v)} @@ -598,7 +516,7 @@ export default function BackupsPage() { { value: "backup", label: ".backup" }, ]} /> - +
@@ -609,7 +527,7 @@ export default function BackupsPage() { }>Хранилище - + setStore("type", v)} @@ -620,45 +538,45 @@ export default function BackupsPage() { { value: "smb", label: "SMB" }, ]} /> - +
{storage.type === "local" && ( - + setStore("localPath", e.target.value)} /> - + )} {storage.type !== "local" && ( <>
- + setStore("host", e.target.value)} /> - +
- + setStore("port", e.target.value)} /> - +
{storage.type === "smb" && ( - + setStore("share", e.target.value)} /> - + )}
- + setStore("username", e.target.value)} /> - - + +
-
+
- + setStore("remotePath", e.target.value)} /> - + )} @@ -759,71 +677,129 @@ export default function BackupsPage() {
{/* ══ Sheet: Manual backup ══════════════════════════════════════════════ */} - + { setManualOpen(v); if (!v) setManualStep(1) }}> Новый бэкап Снять конфигурацию вручную с выбранных серверов -
-
-
-

Выберите серверы

-
- - · - + + + + + 1 + Серверы + + + + + + 2 + Заметка + + + + + + 3 + Подтверждение + + + + + +
+

Выберите серверы

+
+ + · + +
-
- {liveServers.map((s) => { - const checked = manualServers.has(s.id) - return ( -
- {s.status === "offline" && ( - недоступен - )} - - ) - })} -
- -
- - setManualNotes(e.target.value)} /> -
-
+
+

{s.name}

+
+ {s.host} + +
+
+ {s.status === "offline" && ( + недоступен + )} + + ) + })} +
+ + + setManualNotes(e.target.value)} /> + + + +

+ Будет создан бэкап для {manualServers.size} серверов. +

+ {manualNotes && ( +

Заметка: {manualNotes}

+ )} +
+ + }>Отмена - + {manualStep > 1 && ( + + )} + {manualStep < 3 ? ( + + ) : ( + + )} + + { + toast.success(`Файл ${files[0]?.name} подготовлен к восстановлению на ${restoreTarget?.server ?? "сервер"}`) + }} + /> ) } diff --git a/app/(main)/bgp/page.tsx b/app/(main)/bgp/page.tsx index e03dcbb..c3c56fe 100644 --- a/app/(main)/bgp/page.tsx +++ b/app/(main)/bgp/page.tsx @@ -2,6 +2,13 @@ import { Fragment, useState, useMemo, useEffect } from "react" import { PageHeader } from "@/components/page-header" +import { DataPageToolbar } from "@/components/data-page-toolbar" +import { BgpSessionsDataGrid } from "@/components/data-grids/bgp-sessions-data-grid" +import type { Filter } from "@/components/reui/filters" +import { applyReuiFilters } from "@/lib/data-filters/apply-reui-filters" +import { BGP_FILTER_FIELDS } from "@/lib/data-filters/bgp-filter-fields" +import type { BgpSessionRow, BgpState, BgpType } from "@/lib/bgp/types" +import { BGP_AS_NAMES } from "@/lib/bgp/types" import { Card, CardContent } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -17,49 +24,12 @@ import { useDataSource } from "@/lib/data-source" // ─── types ──────────────────────────────────────────────────────────────────── -type BgpState = "Established" | "Active" | "Idle" | "Connect" | "OpenSent" | "OpenConfirm" -type BgpType = "eBGP" | "iBGP" -type BgpAfi = "IPv4 Unicast" | "IPv6 Unicast" | "VPNv4 Unicast" +type BgpSession = BgpSessionRow type BgpTab = "sessions" | "routers" | "analytics" type StateFilter = "all" | BgpState type TypeFilter = "all" | BgpType -interface BgpSession { - id: string - serverId: string - serverLabel: string - serverSite: string - peerIp: string - remoteAs: number - localAs: number - routerId: string - description: string - state: BgpState - type: BgpType - afi: BgpAfi - uptime: string | null - holdTime: number - keepalive: number - prefixesRx: number - prefixesTx: number - prefixesActive: number - inputMessages: number - outputMessages: number - capabilities: string[] - lastError: string | null -} - -// ─── AS name lookup ─────────────────────────────────────────────────────────── - -const AS_NAMES: Record = { - 8359: "МТС / Tele2", - 13238: "Яндекс", - 12389: "Ростелеком", - 24940: "Hetzner", - 6777: "AMS-IX", - 1299: "Telia", - 65001: "iBGP internal", -} +const AS_NAMES = BGP_AS_NAMES // ─── mock data ──────────────────────────────────────────────────────────────── const SESSIONS: BgpSession[] = [ @@ -240,13 +210,6 @@ function TypeBadge({ type }: { type: BgpType }) { ) } -function CapChip({ cap }: { cap: string }) { - return ( - - {cap} - - ) -} function fmtNum(n: number) { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` @@ -254,108 +217,6 @@ function fmtNum(n: number) { return String(n) } -function PrefixBar({ rx, tx, active }: { rx: number; tx: number; active: number }) { - const max = Math.max(rx, 1) - return ( -
- {[ - { label: "Получено", val: rx, color: "bg-[var(--chart-rx)]", w: rx / max }, - { label: "Активных", val: active, color: "bg-[var(--chart-1)]", w: active / max }, - { label: "Отправлено", val: tx, color: "bg-[var(--chart-tx)]", w: Math.min(tx / max, 1) }, - ].map(r => ( -
- {r.label} -
-
0 ? 2 : 0)}%` }} /> -
- {fmtNum(r.val)} -
- ))} -
- ) -} - -// ─── RSC snippet ────────────────────────────────────────────────────────────── - -function rscSnippet(s: BgpSession) { - return `/routing bgp connection\nadd name=peer-as${s.remoteAs} remote.address=${s.peerIp}/32 \\\n remote.as=${s.remoteAs} local.role=${s.type === "eBGP" ? "ebgp" : "ibgp"} \\\n output.filter-chain=export-filter input.filter=import-filter \\\n routing-table=main` -} - -// ─── session expanded row ───────────────────────────────────────────────────── - -function SessionDetail({ s }: { s: BgpSession }) { - const [copied, setCopied] = useState(false) - function copy() { - navigator.clipboard.writeText(rscSnippet(s)).then(() => { - setCopied(true); setTimeout(() => setCopied(false), 1800) - }) - } - return ( -
-
- {[ - { label: "Router ID", value: s.routerId }, - { label: "Hold / KA", value: `${s.holdTime}s / ${s.keepalive}s` }, - { label: "AFI/SAFI", value: s.afi }, - { label: "Сообщения ↓/↑", value: `${fmtNum(s.inputMessages)} / ${fmtNum(s.outputMessages)}` }, - ].map(({ label, value }) => ( -
-

{label}

-

{value}

-
- ))} -
- - {/* prefix bars */} - {s.state === "Established" && ( -
-

Префиксы

- -
- )} - - {/* capabilities */} - {s.capabilities.length > 0 && ( -
-

Capabilities

-
- {s.capabilities.map(c => )} -
-
- )} - - {/* last error */} - {s.lastError && ( -
- -

{s.lastError}

-
- )} - - {/* rsc export */} -
-

RouterOS Export

-
-
-            {rscSnippet(s)}
-          
- -
-
-
- ) -} - // ─── backend mapping ────────────────────────────────────────────────────────── interface BackendBgpSession { @@ -397,158 +258,81 @@ function backendToFrontend(b: BackendBgpSession): BgpSession { // ─── sessions tab ───────────────────────────────────────────────────────────── -const STATE_FILTERS: Array<{ value: StateFilter; label: string }> = [ - { value: "all", label: "Все" }, - { value: "Established", label: "Established" }, - { value: "Active", label: "Active" }, - { value: "Idle", label: "Idle" }, - { value: "OpenSent", label: "OpenSent" }, -] +const BGP_FILTER_ACCESSORS = { + state: (s: BgpSession) => s.state, + type: (s: BgpSession) => s.type, + afi: (s: BgpSession) => s.afi, +} function SessionsTab({ sessions }: { sessions: BgpSession[] }) { - const [search, setSearch] = useState("") + const [search, setSearch] = useState("") const [stateFilter, setStateFilter] = useState("all") - const [typeFilter, setTypeFilter] = useState("all") - const [expandedId, setExpandedId] = useState(null) + const [typeFilter, setTypeFilter] = useState("all") + const [advancedFilters, setAdvancedFilters] = useState([]) const q = search.toLowerCase() - const filtered = useMemo(() => sessions.filter(s => { - if (stateFilter !== "all" && s.state !== stateFilter) return false - if (typeFilter !== "all" && s.type !== typeFilter) return false - if (q && !s.peerIp.includes(q) && !s.description.toLowerCase().includes(q) - && !s.serverLabel.includes(q) && !String(s.remoteAs).includes(q) - && !(AS_NAMES[s.remoteAs] ?? "").toLowerCase().includes(q)) return false - return true - }), [sessions, q, stateFilter, typeFilter]) + const filtered = useMemo(() => { + const base = sessions.filter((s) => { + if (stateFilter !== "all" && s.state !== stateFilter) return false + if (typeFilter !== "all" && s.type !== typeFilter) return false + if ( + q && + !s.peerIp.includes(q) && + !s.description.toLowerCase().includes(q) && + !s.serverLabel.includes(q) && + !String(s.remoteAs).includes(q) && + !(AS_NAMES[s.remoteAs] ?? "").toLowerCase().includes(q) + ) { + return false + } + return true + }) + return applyReuiFilters(base, advancedFilters, BGP_FILTER_ACCESSORS) + }, [sessions, q, stateFilter, typeFilter, advancedFilters]) return (
- - {/* filter bar */} -
- {/* search */} -
- - setSearch(e.target.value)} - placeholder="IP, AS, описание…" - className="h-8 pl-8 pr-8 w-52 text-xs" - /> - {search && ( - - )} -
- - {/* state filter */} -
- {STATE_FILTERS.map(f => ( - - ))} -
- - {/* type filter */} -
- {(["all", "eBGP", "iBGP"] as const).map(t => ( - - ))} -
- - - {filtered.length} из {sessions.length} - -
- - {/* table */} -
- - - - - ))} - - - - {filtered.map(s => { - const isOpen = expandedId === s.id - return ( - - setExpandedId(isOpen ? null : s.id)} - className={cn( - "cursor-pointer transition-colors", - isOpen ? "bg-muted/30" : "hover:bg-muted/20", - )}> - - - - - - - - - - - - {isOpen && ( - - - - )} - - ) - })} - - {filtered.length === 0 && ( - - - - )} - -
- {["Роутер", "Peer IP", "Remote AS", "Описание", "Тип", "Состояние", "Uptime", "Prefixes ↓", "Prefixes ↑"].map(h => ( - {h}
- {isOpen - ? - : } - {s.serverLabel}{s.peerIp} -
- AS{s.remoteAs} - {AS_NAMES[s.remoteAs] && ( - {AS_NAMES[s.remoteAs]} - )} -
-
{s.description} - {s.uptime ?? "—"} - - {s.prefixesRx > 0 - ? {fmtNum(s.prefixesRx)} - : } - - {s.prefixesTx > 0 - ? {fmtNum(s.prefixesTx)} - : } -
- -
- Нет сессий по заданным фильтрам -
-
+ s.state === "Established").length }, + { value: "Active", label: "Active", count: sessions.filter((s) => s.state === "Active").length }, + { value: "Idle", label: "Idle", count: sessions.filter((s) => s.state === "Idle").length }, + { value: "OpenSent", label: "OpenSent", count: sessions.filter((s) => s.state === "OpenSent").length }, + ], + }} + filters={advancedFilters} + onFiltersChange={setAdvancedFilters} + filterFields={BGP_FILTER_FIELDS} + search={search} + onSearchChange={setSearch} + searchPlaceholder="IP, AS, описание…" + countLabel={`${filtered.length} из ${sessions.length}`} + actions={ +
+ {(["all", "eBGP", "iBGP"] as const).map((t) => ( + + ))} +
+ } + /> +
) diff --git a/app/(main)/certificates/page.tsx b/app/(main)/certificates/page.tsx index db99095..108c0fe 100644 --- a/app/(main)/certificates/page.tsx +++ b/app/(main)/certificates/page.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react" import { PageHeader } from "@/components/page-header" +import { FormField, FormToggle, SectionTitle } from "@/components/form-kit" +import { FileImportDialog } from "@/components/file-import-dialog" import { routerCertificates, servers as mockServers } from "@/lib/data" import type { CertStatus, Server } from "@/lib/data" import type { CertificateDto } from "@mmapp/contracts/certificates" @@ -47,7 +49,19 @@ import { ChevronDownIcon, ChevronRightIcon, RefreshCwIcon, + UploadIcon, } from "lucide-react" +import { + Stepper, + StepperContent, + StepperIndicator, + StepperItem, + StepperNav, + StepperPanel, + StepperSeparator, + StepperTitle, + StepperTrigger, +} from "@/components/reui/stepper" const STATUS_CONFIG: Record< CertStatus, @@ -93,60 +107,6 @@ function daysLeftBar(days: number, total = 365): number { return Math.min(100, Math.round((days / total) * 100)) } -function Field({ - label, - hint, - required, - children, -}: { - label: string - hint?: string - required?: boolean - children: ReactNode -}) { - return ( -
- - {children} - {hint &&

{hint}

} -
- ) -} - -function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { - return ( - - ) -} - -function SectionTitle({ children }: { children: ReactNode }) { - return ( -
- {children} -
-
- ) -} - function mockToDto(cert: (typeof routerCertificates)[number]): CertificateDto { return { id: cert.id, @@ -679,6 +639,7 @@ function CertPartIssueForm({ setIssueTrustWww, issueTrustApi, setIssueTrustApi, + step, }: { serverList: Server[] issueServerId: string @@ -693,12 +654,15 @@ function CertPartIssueForm({ setIssueTrustWww: (v: boolean) => void issueTrustApi: boolean setIssueTrustApi: (v: boolean) => void + step?: 1 | 2 | 3 | 4 }) { + const showAll = step == null return (
+ {(showAll || step === 1) && (
Основные - + - - + + setIssueCertName(e.target.value)} placeholder="router-le" /> - +
+ )} + {(showAll || step === 2) && (
Домены - + setIssueCommonName(e.target.value)} placeholder="vpn.example.com" /> - - + +