fix(ui): убрать неподтверждённые данные и действия GRE-пулов
This commit is contained in:
1 parent
337d14abdf
commit
fc3d588cec
6 files changed
+55
-139
No files matched your search
+13
-83
@@ -11,7 +11,7 @@ import { GreTunnelsDataGrid } from "@/components/data-grids/gre-tunnels-data-gri
|
||||
import { GrePoolsDataGrid } from "@/components/data-grids/gre-pools-data-grid"
|
||||
import { FormField, FormToggle, SectionTitle } from "@/components/form-kit"
|
||||
import { greTunnels as mockGreTunnels, grePools as mockGrePools, servers as mockServers } from "@/lib/data"
|
||||
import type { GrePool, GreTunnel, GreStatus, Server } from "@/lib/data"
|
||||
import type { GreTunnel, GreStatus, Server } from "@/lib/data"
|
||||
import { useDataSource } from "@/lib/data-source"
|
||||
import { generateGreRsc } from "@/lib/gre-export"
|
||||
import { greResponseSchema } from "@/lib/gre-response"
|
||||
@@ -96,22 +96,6 @@ function mapBackendToServer(s: BackendServer): InventoryServer {
|
||||
}
|
||||
}
|
||||
|
||||
function derivePoolsFromTunnels(tunnels: GreTunnel[]): GrePool[] {
|
||||
const ids = [...new Set(tunnels.map((t) => t.poolId).filter(Boolean))]
|
||||
return ids.map((id) => {
|
||||
const list = tunnels.filter((t) => t.poolId === id)
|
||||
const allocated = list.length
|
||||
return {
|
||||
id,
|
||||
name: id === "live" ? "С устройств (опрос)" : id,
|
||||
cidr: "—",
|
||||
allocated,
|
||||
total: Math.max(allocated, 1),
|
||||
comment: "По poolId из данных GRE",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ─── default form states ─────────────────────────────────────────────────────
|
||||
|
||||
const defaultTunnelForm = {
|
||||
@@ -123,7 +107,6 @@ const defaultTunnelForm = {
|
||||
showAdvanced: false,
|
||||
}
|
||||
|
||||
const defaultPoolForm = { name: "", cidr: "", comment: "" }
|
||||
|
||||
type TabFilter = "all" | "up" | "ipsec" | "plain"
|
||||
type PageTab = "tunnels" | "pools"
|
||||
@@ -180,12 +163,10 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
const [historyError, setHistoryError] = useState<string | null>(null)
|
||||
const historyEpoch = useRef(0)
|
||||
const [historyRestoring, setHistoryRestoring] = useState(false)
|
||||
const [poolOpen, setPoolOpen] = useState(false)
|
||||
const [codePreviewTunnel, setCodePreviewTunnel] = useState<GreTunnel | null>(null)
|
||||
const [exportContext, setExportContext] = useState<{ servers: Record<string, Server>; at: string | null; stale: boolean; mock: boolean } | null>(null)
|
||||
|
||||
const [tForm, setTForm] = useState(defaultTunnelForm)
|
||||
const [pForm, setPForm] = useState(defaultPoolForm)
|
||||
|
||||
const setT = <K extends keyof typeof defaultTunnelForm>(k: K, v: (typeof defaultTunnelForm)[K]) =>
|
||||
setTForm((f) => ({ ...f, [k]: v }))
|
||||
@@ -240,8 +221,8 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
const displayTunnels = isLive ? liveTunnels : mockGreTunnels
|
||||
const displayServers = isLive ? liveServers : mockServers
|
||||
const displayPools = useMemo(
|
||||
() => (isLive ? derivePoolsFromTunnels(displayTunnels) : mockGrePools),
|
||||
[isLive, displayTunnels],
|
||||
() => (isLive ? [] : mockGrePools),
|
||||
[isLive],
|
||||
)
|
||||
|
||||
const greRailItems = useMemo<ServerTileItem[]>(() => (
|
||||
@@ -670,8 +651,8 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
{
|
||||
id: "pools",
|
||||
label: "IP-пулов",
|
||||
value: incomplete ? "—" : displayPools.length,
|
||||
hint: incomplete ? "неизвестно" : undefined,
|
||||
value: isLive ? "—" : displayPools.length,
|
||||
hint: isLive ? "нет данных API" : "моковые данные",
|
||||
icon: <DatabaseIcon className="size-4" />,
|
||||
iconClassName: "text-primary",
|
||||
},
|
||||
@@ -687,9 +668,9 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
{toggleError && <p role="alert" className="text-sm text-destructive">{toggleError}</p>}
|
||||
|
||||
{/* Page tabs */}
|
||||
<div className="flex items-center gap-1 border-b">
|
||||
<div role="group" aria-label="Раздел GRE" className="flex items-center gap-1 border-b">
|
||||
{(["tunnels", "pools"] as PageTab[]).map((tab) => (
|
||||
<button key={tab} onClick={() => setPageTab(tab)}
|
||||
<button key={tab} aria-pressed={pageTab === tab} onClick={() => setPageTab(tab)}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors -mb-px ${pageTab === tab ? "border-foreground text-foreground" : "border-transparent text-muted-foreground hover:text-foreground"}`}>
|
||||
{tab === "tunnels" ? "Туннели" : "IP-пулы"}
|
||||
</button>
|
||||
@@ -735,13 +716,10 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
{/* ── IP Pools ── */}
|
||||
{pageTab === "pools" && (
|
||||
<DataPageCard>
|
||||
<DataPageToolbarFrame className="justify-between">
|
||||
<span className="text-sm text-muted-foreground">{displayPools.length} пула</span>
|
||||
<Button size="sm" variant="outline" onClick={() => { setPForm(defaultPoolForm); setPoolOpen(true) }}>
|
||||
<PlusIcon className="size-4" />
|
||||
Добавить пул
|
||||
</Button>
|
||||
</DataPageToolbarFrame>
|
||||
{isLive ? (
|
||||
<div role="status" className="p-5 text-sm text-muted-foreground">API GRE возвращает интерфейсы и назначенные IP. Каталог пулов, CIDR и свободная ёмкость не предоставляются; управление пулами здесь недоступно.</div>
|
||||
) : <>
|
||||
<DataPageToolbarFrame className="justify-between"><span className="text-sm text-muted-foreground">Моковый каталог · только просмотр</span></DataPageToolbarFrame>
|
||||
<GrePoolsDataGrid pools={displayPools} />
|
||||
|
||||
<div className="border-t px-5 py-4">
|
||||
@@ -767,6 +745,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>}
|
||||
</DataPageCard>
|
||||
)}
|
||||
|
||||
@@ -884,13 +863,7 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<SectionTitle>Внутренний IP</SectionTitle>
|
||||
<FormField label="IP-пул" hint="Необязательно — внутренний IP можно указать вручную">
|
||||
<select value={tForm.poolId} onChange={(e) => setT("poolId", e.target.value)}
|
||||
className="h-8 w-full rounded-lg border border-input bg-background px-2.5 text-sm text-foreground outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50">
|
||||
<option value="" disabled>Выбрать пул…</option>
|
||||
{displayPools.map((p) => <option key={p.id} value={p.id}>{p.name} ({p.cidr}) — свободно {p.total - p.allocated} блоков</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
{!isLive && <p className="text-xs text-muted-foreground">Моковые пулы доступны только для просмотра.</p>}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FormField label="Локальный IP" required hint="/ip address на этом конце">
|
||||
<Input className="font-mono" placeholder="10.200.0.1/30" value={tForm.localInnerIp} onChange={(e) => setT("localInnerIp", e.target.value)} />
|
||||
@@ -984,49 +957,6 @@ function GrePageContent({ selectedServerId, selectedTunnelId, allTunnelsHref, on
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{/* ══ Sheet: Add Pool ════════════════════════════════════════════════════ */}
|
||||
<Sheet open={poolOpen} onOpenChange={setPoolOpen}>
|
||||
<SheetContent side="right" className="w-full sm:max-w-md flex flex-col gap-0 p-0">
|
||||
<SheetHeader className="px-6 pt-6 pb-4 border-b shrink-0">
|
||||
<SheetTitle>Новый IP-пул</SheetTitle>
|
||||
<SheetDescription>Пул адресов для назначения внутренних IP GRE-туннелям</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 overflow-y-auto px-6 py-5 flex flex-col gap-5">
|
||||
<div className="flex flex-col gap-4">
|
||||
<SectionTitle>Параметры пула</SectionTitle>
|
||||
<FormField label="Имя пула" required hint="Например pool-gre-office или pool-gre-dc2">
|
||||
<Input className="font-mono" placeholder="pool-gre-core" value={pForm.name}
|
||||
onChange={(e) => setPForm((f) => ({ ...f, name: e.target.value }))} />
|
||||
</FormField>
|
||||
<FormField label="Диапазон CIDR" required hint="Блок, из которого будут нарезаться /30 на каждый туннель">
|
||||
<Input className="font-mono" placeholder="10.200.0.0/24" value={pForm.cidr}
|
||||
onChange={(e) => setPForm((f) => ({ ...f, cidr: e.target.value }))} />
|
||||
</FormField>
|
||||
<FormField label="Назначение / Комментарий">
|
||||
<Input placeholder="Ядровые межузловые туннели" value={pForm.comment}
|
||||
onChange={(e) => setPForm((f) => ({ ...f, comment: e.target.value }))} />
|
||||
</FormField>
|
||||
{pForm.cidr && /\/\d+$/.test(pForm.cidr) && (() => {
|
||||
const prefix = parseInt(pForm.cidr.split("/")[1] ?? "0")
|
||||
const blocks = prefix <= 30 ? Math.pow(2, 30 - prefix) : 0
|
||||
return blocks > 0 ? (
|
||||
<div className="rounded-lg border border-border bg-muted/30 px-4 py-3 text-sm">
|
||||
<p className="text-muted-foreground">
|
||||
Доступно <span className="font-semibold text-foreground font-mono">{blocks}</span> блоков /30
|
||||
{" "}= до <span className="font-semibold text-foreground font-mono">{blocks}</span> туннелей
|
||||
</p>
|
||||
</div>
|
||||
) : null
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<SheetFooter className="px-6 py-4 border-t shrink-0 flex-row gap-2">
|
||||
<SheetClose render={<Button variant="outline" className="flex-1" />}>Отмена</SheetClose>
|
||||
<Button className="flex-1" onClick={() => setPoolOpen(false)}>Создать пул</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<ConfigHistorySheet
|
||||
open={historyOpen}
|
||||
onOpenChange={v => { if (!v) historyEpoch.current += 1; setHistoryOpen(v) }}
|
||||
|
||||
Reference in new issue
Block a user