feat(dashboard): собрать ops-дашборд на ReUI Frame с живыми данными
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
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 <cursoragent@cursor.com>
This commit is contained in:
1 parent
5f2b4e2d40
commit
36c5305db7
10 files changed
+1580
-917
No files matched your search
@@ -0,0 +1,130 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user