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
Co-authored-by: Cursor <[email protected]>
98 lines
2.8 KiB
TypeScript
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>
|
|
)
|
|
}
|