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]>
131 lines
4.6 KiB
TypeScript
131 lines
4.6 KiB
TypeScript
"use client"
|
||
|
||
import type { ReactNode } from "react"
|
||
import Link from "next/link"
|
||
import { Badge } from "@/components/reui/badge"
|
||
import { IconTile } from "@/components/reui/icon-tile"
|
||
import { StatusBadge } from "@/components/status-badge"
|
||
import { Flag } from "@/components/flag"
|
||
import type { InternetPathViewModel } from "@/lib/dashboard-internet-path"
|
||
import type { ServerStatus } from "@/lib/data"
|
||
import { cn } from "@/lib/utils"
|
||
import { ArrowRightIcon, HomeIcon, RadioIcon, ServerIcon, GlobeIcon } from "lucide-react"
|
||
|
||
function pathStateToServerStatus(state: InternetPathViewModel["pathState"]): ServerStatus {
|
||
if (state === "healthy") return "online"
|
||
if (state === "failover" || state === "degraded") return "degraded"
|
||
return "offline"
|
||
}
|
||
|
||
function HopChip({
|
||
label,
|
||
name,
|
||
country,
|
||
icon,
|
||
iconClassName,
|
||
}: {
|
||
label: string
|
||
name: string
|
||
country?: string
|
||
icon: ReactNode
|
||
iconClassName?: string
|
||
}) {
|
||
return (
|
||
<div className="flex min-w-0 items-center gap-2 rounded-md border border-border/60 px-2 py-1.5">
|
||
<IconTile variant="elevated" size="sm" className={cn("shrink-0", iconClassName)} aria-hidden="true">
|
||
{icon}
|
||
</IconTile>
|
||
<div className="min-w-0">
|
||
<p className="text-muted-foreground text-[10px] font-medium uppercase tracking-wide">{label}</p>
|
||
<p className="flex items-center gap-1 truncate text-sm font-medium">
|
||
{country ? <Flag code={country} className="shrink-0" /> : null}
|
||
<span className="truncate">{name}</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Compact live path strip (Frame-friendly). Canvas lives separately.
|
||
* Preview: https://reui.io/preview/base/stats-12
|
||
* Docs: https://reui.io/docs/components/base/icon-tile
|
||
*/
|
||
export function InternetPathSummary({ model }: { model: InternetPathViewModel | null }) {
|
||
if (!model) {
|
||
return (
|
||
<p className="text-muted-foreground text-sm">
|
||
Недостаточно данных для пути. Добавьте home-router и проверьте{" "}
|
||
<Link href="/network-map" className="underline underline-offset-2">
|
||
карту сети
|
||
</Link>
|
||
.
|
||
</p>
|
||
)
|
||
}
|
||
|
||
const hop = model.currentHop ?? model.primaryHop
|
||
const wanName = hop?.wan.name ?? model.activeWanUplink?.name ?? "WAN"
|
||
const wanIsp = hop?.wan.isp ?? model.activeWanUplink?.isp ?? "—"
|
||
const ping = hop?.wanJhMetrics.pingMs
|
||
const dl = hop?.wanJhMetrics.dlMbps
|
||
|
||
return (
|
||
<div className="flex flex-col gap-3">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<StatusBadge status={pathStateToServerStatus(model.pathState)} />
|
||
{model.pathState === "failover" ? (
|
||
<Badge variant="warning-light" size="sm">failover</Badge>
|
||
) : null}
|
||
{ping != null ? (
|
||
<Badge variant="outline" size="sm" className="tabular-nums">
|
||
{ping} мс
|
||
</Badge>
|
||
) : null}
|
||
{dl != null ? (
|
||
<Badge variant="outline" size="sm" className="tabular-nums">
|
||
{Math.round(dl)} ↓ Мбит/с
|
||
</Badge>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-2 @3xl:flex-row @3xl:items-center">
|
||
<HopChip
|
||
label="Home"
|
||
name={model.homeRouter.name}
|
||
country={model.homeRouter.country}
|
||
icon={<HomeIcon />}
|
||
iconClassName="text-success"
|
||
/>
|
||
<ArrowRightIcon className="text-muted-foreground hidden size-4 shrink-0 @3xl:block" aria-hidden="true" />
|
||
<HopChip
|
||
label="WAN"
|
||
name={`${wanName} · ${wanIsp}`}
|
||
icon={<RadioIcon />}
|
||
iconClassName="text-info"
|
||
/>
|
||
<ArrowRightIcon className="text-muted-foreground hidden size-4 shrink-0 @3xl:block" aria-hidden="true" />
|
||
<HopChip
|
||
label="JH"
|
||
name={hop?.jumpHost.name ?? model.fallbackJumpHost?.name ?? "—"}
|
||
country={hop?.jumpHost.country ?? model.fallbackJumpHost?.country}
|
||
icon={<ServerIcon />}
|
||
iconClassName="text-primary"
|
||
/>
|
||
<ArrowRightIcon className="text-muted-foreground hidden size-4 shrink-0 @3xl:block" aria-hidden="true" />
|
||
<HopChip
|
||
label="Exit"
|
||
name={hop?.exitNode.name ?? model.fallbackExitNode?.name ?? "—"}
|
||
country={hop?.exitNode.country ?? model.fallbackExitNode?.country}
|
||
icon={<GlobeIcon />}
|
||
iconClassName="text-muted-foreground"
|
||
/>
|
||
</div>
|
||
|
||
<p className="text-muted-foreground text-xs leading-relaxed">
|
||
{model.currentPath?.reason ?? model.primaryPath?.reason ?? "Текущий путь не определён"}
|
||
</p>
|
||
</div>
|
||
)
|
||
}
|