Docker images / prepare-release (push) Successful in 7s
Docker images / backend-image (push) Successful in 1m37s
Docker images / frontend-image (push) Successful in 2m44s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 40s
Docker images / publish-release (push) Successful in 10s
Added support for managing peer information in interface bindings, including new fields for peerPublicKey and peerName in the BoundIfaceTraffic interface. Updated the database schema to include these fields in user_interface_bindings and traffic_samples tables. Enhanced the UI components to display peer details alongside interface names, improving user experience and clarity in the traffic management system. Updated relevant functions and services to handle peer-specific logic, ensuring robust integration across the application.
133 lines
5.2 KiB
TypeScript
133 lines
5.2 KiB
TypeScript
"use client"
|
|
|
|
import { Badge } from "@/components/reui/badge"
|
|
import { IconTile } from "@/components/reui/icon-tile"
|
|
import { Flag } from "@/components/flag"
|
|
import { cn } from "@/lib/utils"
|
|
import {
|
|
ALL_SECTIONS,
|
|
groupBindingsByServer,
|
|
IFACE_TYPE_LABEL,
|
|
summarizeUserAccess,
|
|
type AppUser,
|
|
type InterfaceType,
|
|
} from "@/lib/users"
|
|
import { CableIcon, KeyRoundIcon, NetworkIcon, ShieldIcon } from "lucide-react"
|
|
|
|
const TYPE_VARIANT: Record<InterfaceType, "outline" | "info-light" | "success-light" | "secondary"> = {
|
|
ether: "outline",
|
|
gre: "info-light",
|
|
wg: "success-light",
|
|
other: "secondary",
|
|
}
|
|
|
|
const TYPE_ICON: Record<InterfaceType, { icon: typeof CableIcon; className: string }> = {
|
|
ether: { icon: CableIcon, className: "text-muted-foreground" },
|
|
gre: { icon: NetworkIcon, className: "text-info" },
|
|
wg: { icon: ShieldIcon, className: "text-success" },
|
|
other: { icon: CableIcon, className: "text-muted-foreground" },
|
|
}
|
|
|
|
interface UsersExpandedDetailProps {
|
|
user: AppUser
|
|
serversCount: number
|
|
allSectionsCount?: number
|
|
}
|
|
|
|
function UsersExpandedDetail({
|
|
user,
|
|
serversCount,
|
|
allSectionsCount = ALL_SECTIONS.length,
|
|
}: UsersExpandedDetailProps) {
|
|
const access = summarizeUserAccess(user, serversCount, allSectionsCount)
|
|
const groups = groupBindingsByServer(user.bindings)
|
|
|
|
return (
|
|
<div className="flex flex-col gap-5 px-5 py-5 bg-muted/20">
|
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-xs">
|
|
<span className="text-muted-foreground">
|
|
Разделы:{" "}
|
|
<span className="font-mono text-foreground">
|
|
{access.sectionsGranted}/{access.sectionsTotal}
|
|
</span>
|
|
</span>
|
|
<span className="text-muted-foreground">
|
|
Серверы:{" "}
|
|
<span className="font-mono text-foreground">
|
|
{access.serversGranted}/{access.serversTotal}
|
|
</span>
|
|
</span>
|
|
{access.writeKind === "full" ? (
|
|
<Badge variant="success-light" size="sm">Полный доступ</Badge>
|
|
) : access.writeKind === "none" ? (
|
|
<Badge variant="outline" size="sm">Только просмотр</Badge>
|
|
) : (
|
|
<span className="inline-flex flex-wrap items-center gap-1">
|
|
{access.writeSections.map((section) => (
|
|
<Badge key={section} variant="info-light" size="sm">{section}</Badge>
|
|
))}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{groups.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground">Нет привязанных интерфейсов</p>
|
|
) : (
|
|
groups.map((g) => {
|
|
const items = g.types.flatMap((tg) => tg.items)
|
|
return (
|
|
<div key={g.serverId}>
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<p className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider">
|
|
{g.serverName}
|
|
</p>
|
|
<Flag code={g.serverCountry} size={12} />
|
|
<span className="text-[11px] font-mono text-muted-foreground">{g.serverSite}</span>
|
|
</div>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 xl:grid-cols-5 gap-2">
|
|
{items.map((b) => {
|
|
const meta = TYPE_ICON[b.interfaceType]
|
|
const Icon = b.interfaceType === "wg" && b.peerPublicKey ? KeyRoundIcon : meta.icon
|
|
const iconClass = b.interfaceType === "wg" && b.peerPublicKey ? "text-success" : meta.className
|
|
return (
|
|
<div
|
|
key={b.id}
|
|
className="flex items-start gap-2 rounded-md border px-3 py-2.5"
|
|
>
|
|
<IconTile variant="elevated" className={cn("size-10.5 shrink-0", iconClass)}>
|
|
<Icon />
|
|
</IconTile>
|
|
<div className="min-w-0">
|
|
<p className="text-xs font-mono font-medium leading-tight truncate">
|
|
{b.interfaceType === "wg" && (b.peerName || b.peerPublicKey)
|
|
? `${b.peerName || "peer"} · ${b.interfaceName}`
|
|
: b.interfaceName}
|
|
</p>
|
|
<div className="flex flex-wrap items-center gap-1 mt-0.5">
|
|
<Badge variant={TYPE_VARIANT[b.interfaceType]} size="sm">
|
|
{IFACE_TYPE_LABEL[b.interfaceType]}
|
|
</Badge>
|
|
{b.interfaceType === "wg" && !(b.peerPublicKey ?? "") ? (
|
|
<Badge variant="warning-light" size="sm">весь интерфейс</Badge>
|
|
) : null}
|
|
{b.comment ? (
|
|
<span className="text-[10px] text-muted-foreground leading-tight truncate">
|
|
{b.comment}
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export { UsersExpandedDetail, TYPE_VARIANT, type UsersExpandedDetailProps }
|