mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-09-13 10:49:21 +00:00
216 lines
5.9 KiB
TypeScript
216 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { Handle, Position } from "@xyflow/react";
|
|
import { Check, Copy } from "lucide-react";
|
|
import type { MouseEvent, ReactNode } from "react";
|
|
import { useState } from "react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/cn";
|
|
import type { HealthStatus } from "@/lib/topology-types";
|
|
|
|
const healthLabels: Record<HealthStatus, string> = {
|
|
healthy: "Healthy",
|
|
degraded: "Degraded",
|
|
unhealthy: "Unhealthy",
|
|
unknown: "Unknown",
|
|
};
|
|
|
|
const solidHealthClasses: Record<Exclude<HealthStatus, "unknown">, string> = {
|
|
healthy: "border-transparent bg-success text-success-foreground hover:bg-success/90",
|
|
degraded: "border-transparent bg-warning text-warning-foreground hover:bg-warning/90",
|
|
unhealthy:
|
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
};
|
|
|
|
interface HealthBadgeProps {
|
|
status: HealthStatus;
|
|
appearance?: "solid" | "summary" | "detail";
|
|
children?: ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function HealthBadge({
|
|
status,
|
|
appearance = "solid",
|
|
children,
|
|
className,
|
|
}: HealthBadgeProps) {
|
|
const content = children ?? healthLabels[status];
|
|
|
|
if (appearance === "summary") {
|
|
const indicatorClasses = {
|
|
healthy: "bg-success",
|
|
degraded: "bg-warning",
|
|
unhealthy: "bg-destructive",
|
|
unknown: "bg-muted-foreground",
|
|
}[status];
|
|
|
|
return (
|
|
<Badge variant="outline" className={cn("flex items-center gap-1", className)}>
|
|
<span className={cn("h-2 w-2 rounded-full", indicatorClasses)} />
|
|
{content}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
if (appearance === "detail") {
|
|
return (
|
|
<Badge
|
|
variant={
|
|
status === "healthy" ? "default" : status === "degraded" ? "secondary" : "destructive"
|
|
}
|
|
className={className}
|
|
>
|
|
{content}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
if (status === "unknown") {
|
|
return (
|
|
<Badge variant="secondary" className={cn("text-xs", className)}>
|
|
{content}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
return <Badge className={cn(solidHealthClasses[status], "text-xs", className)}>{content}</Badge>;
|
|
}
|
|
|
|
interface TopologyNodeCardProps {
|
|
selected: boolean;
|
|
icon: ReactNode;
|
|
iconClassName: string;
|
|
title: ReactNode;
|
|
description?: ReactNode;
|
|
health: HealthStatus;
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function TopologyNodeCard({
|
|
selected,
|
|
icon,
|
|
iconClassName,
|
|
title,
|
|
description,
|
|
health,
|
|
children,
|
|
}: TopologyNodeCardProps) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"rounded-lg border bg-card p-3 shadow-md hover:shadow-lg transition-all w-[300px]",
|
|
selected && "ring-2 ring-primary ring-offset-2 ring-offset-background shadow-xl"
|
|
)}
|
|
>
|
|
<Handle type="target" position={Position.Top} className="h-3 w-3 !bg-muted-foreground" />
|
|
<div className="space-y-2.5">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
<div className={cn("rounded-md p-1.5", iconClassName)}>{icon}</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-bold text-sm truncate">{title}</p>
|
|
{description && (
|
|
<p className="text-xs text-muted-foreground truncate">{description}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<HealthBadge status={health} />
|
|
</div>
|
|
{children}
|
|
</div>
|
|
<Handle type="source" position={Position.Bottom} className="h-3 w-3 !bg-muted-foreground" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface CompactRowProps {
|
|
label: ReactNode;
|
|
icon?: ReactNode;
|
|
children?: ReactNode;
|
|
className?: string;
|
|
valueClassName?: string;
|
|
}
|
|
|
|
export function CompactRow({ label, icon, children, className, valueClassName }: CompactRowProps) {
|
|
return (
|
|
<div className={cn("flex items-center justify-between", className)}>
|
|
<span className={cn("text-muted-foreground", icon && "flex items-center gap-1")}>
|
|
{icon}
|
|
{label}
|
|
</span>
|
|
{children !== undefined && (
|
|
<span className={cn("font-medium", valueClassName)}>{children}</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface MetricRowProps {
|
|
label: string;
|
|
icon: ReactNode;
|
|
usage?: string;
|
|
limit: ReactNode;
|
|
}
|
|
|
|
export function MetricRow({ label, icon, usage, limit }: MetricRowProps) {
|
|
return (
|
|
<CompactRow label={label} icon={icon} valueClassName="text-xs">
|
|
{usage && <span className="text-info">{usage} / </span>}
|
|
<span className="text-muted-foreground">{limit}</span>
|
|
</CompactRow>
|
|
);
|
|
}
|
|
|
|
export function DetailSection({ title, children }: { title: string; children: ReactNode }) {
|
|
return (
|
|
<div className="space-y-2">
|
|
<h3 className="font-semibold text-sm">{title}</h3>
|
|
<div className="space-y-2">{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function DetailRow({ label, value }: { label: string; value: ReactNode }) {
|
|
return (
|
|
<div className="flex justify-between items-start text-sm gap-4">
|
|
<span className="text-muted-foreground shrink-0">{label}:</span>
|
|
<span className="font-medium text-right break-words">{value}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface CopyableCodeProps {
|
|
value: string;
|
|
title?: string;
|
|
}
|
|
|
|
export function CopyableCode({ value, title = "Copy address" }: CopyableCodeProps) {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopy = async (event: MouseEvent<HTMLButtonElement>) => {
|
|
event.stopPropagation();
|
|
await navigator.clipboard.writeText(value);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
<code className="text-[10px] bg-muted px-1.5 py-0.5 rounded font-mono flex-1 truncate">
|
|
{value}
|
|
</code>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-5 w-5 shrink-0"
|
|
onClick={handleCopy}
|
|
title={copied ? "Copied!" : title}
|
|
>
|
|
{copied ? <Check className="h-3 w-3 text-success" /> : <Copy className="h-3 w-3" />}
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|