Files
MikrotikManager/components/dashboard/internet-path-panel.tsx
T
DenozordecandCursor 36c5305db7
Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m13s
Docker images / frontend-image (push) Successful in 3m14s
Docker images / updater-image (push) Successful in 47s
Docker images / backend-image (push) Successful in 2m44s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 14s
feat(dashboard): собрать ops-дашборд на ReUI Frame с живыми данными
Co-authored-by: Cursor <[email protected]>
2026-09-08 15:13:27 +07:00

98 lines
2.8 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import Link from "next/link"
import { ChevronDownIcon } from "lucide-react"
import { OpsPanel } from "@/components/ops-panel"
import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert"
import { buttonVariants } from "@/components/ui/button"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { cn } from "@/lib/utils"
import type { InternetPathViewModel } from "@/lib/dashboard-internet-path"
import { InternetPathMapCanvas } from "@/components/dashboard/internet-path-map"
import { InternetPathSummary } from "@/components/dashboard/internet-path-summary"
const PATH_MAP_OPEN_LS = "mm:dashboard-path-map-open"
function readMapOpen(): boolean {
if (typeof window === "undefined") return true
try {
const raw = localStorage.getItem(PATH_MAP_OPEN_LS)
if (raw === "0") return false
if (raw === "1") return true
} catch {
/* ignore */
}
return true
}
export function InternetPathPanel({
model,
loading,
error,
}: {
model: InternetPathViewModel | null
loading?: boolean
error?: string | null
}) {
const [open, setOpen] = useState(true)
const [hydrated, setHydrated] = useState(false)
useEffect(() => {
queueMicrotask(() => {
setOpen(readMapOpen())
setHydrated(true)
})
}, [])
function handleOpenChange(next: boolean) {
setOpen(next)
try {
localStorage.setItem(PATH_MAP_OPEN_LS, next ? "1" : "0")
} catch {
/* ignore */
}
}
return (
<OpsPanel
title="Internet path"
description="Home → WAN → JH → Exit"
headerRight={
<Link href="/network-map" className={cn(buttonVariants({ variant: "outline", size: "sm" }), "h-7 text-xs")}>
Карта сети
</Link>
}
contentClassName="flex flex-col gap-3 px-5 pb-4"
>
{error ? (
<Alert variant="destructive">
<AlertTitle>Не удалось загрузить путь</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
{loading && !model ? (
<div className="h-20 animate-pulse rounded-md bg-muted/40" />
) : (
<InternetPathSummary model={model} />
)}
<Collapsible open={hydrated ? open : true} onOpenChange={handleOpenChange}>
<CollapsibleTrigger
className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "h-7 w-fit gap-1.5 text-xs")}
>
<ChevronDownIcon className={cn("size-3.5 transition-transform", open && "rotate-180")} />
{open ? "Скрыть карту" : "Показать карту"}
</CollapsibleTrigger>
<CollapsibleContent>
<InternetPathMapCanvas model={model} />
</CollapsibleContent>
</Collapsible>
</OpsPanel>
)
}