92 lines
3.1 KiB
TypeScript
92 lines
3.1 KiB
TypeScript
import { forwardRef } from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
|
|
export type NodeStatus = 'ok' | 'error' | 'na' | 'inactive';
|
|
|
|
interface TopologyNodeProps {
|
|
label: string;
|
|
sublabel?: string;
|
|
status?: NodeStatus;
|
|
icon?: React.ReactNode;
|
|
onClick?: () => void;
|
|
interactive?: boolean;
|
|
disabled?: boolean;
|
|
tooltip?: React.ReactNode;
|
|
className?: string;
|
|
children?: React.ReactNode;
|
|
}
|
|
|
|
const statusColors: Record<NodeStatus, string> = {
|
|
ok: 'border-green-500/60 bg-green-50/50 dark:bg-green-950/20',
|
|
error: 'border-red-500/60 bg-red-50/50 dark:bg-red-950/20',
|
|
na: 'border-muted-foreground/30 bg-muted/30',
|
|
inactive: 'border-dashed border-muted-foreground/20 bg-muted/10 opacity-50',
|
|
};
|
|
|
|
const statusDotColors: Record<NodeStatus, string> = {
|
|
ok: 'bg-green-500',
|
|
error: 'bg-red-500',
|
|
na: 'bg-muted-foreground/40',
|
|
inactive: 'bg-muted-foreground/20',
|
|
};
|
|
|
|
export const TopologyNode = forwardRef<HTMLDivElement, TopologyNodeProps>(
|
|
function TopologyNode(
|
|
{ label, sublabel, status = 'ok', icon, onClick, interactive, disabled, tooltip, className, children },
|
|
ref,
|
|
) {
|
|
const isClickable = (interactive || onClick) && !disabled;
|
|
|
|
const content = (
|
|
<div
|
|
ref={ref}
|
|
role={isClickable ? 'button' : undefined}
|
|
tabIndex={isClickable ? 0 : undefined}
|
|
onClick={isClickable ? onClick : undefined}
|
|
// Stop pointerdown propagation so React Flow doesn't swallow the click
|
|
onPointerDown={isClickable ? (e) => e.stopPropagation() : undefined}
|
|
onKeyDown={isClickable ? (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick?.(); } } : undefined}
|
|
style={{ pointerEvents: 'all', position: 'relative' }}
|
|
className={cn(
|
|
'rounded-lg border px-3 py-2 text-xs transition-all duration-200',
|
|
'nopan nodrag',
|
|
statusColors[status],
|
|
isClickable && '!cursor-pointer hover:shadow-sm hover:border-primary/40',
|
|
disabled && '!pointer-events-none',
|
|
className,
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{icon && <span className="shrink-0 text-muted-foreground">{icon}</span>}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className={cn('h-1.5 w-1.5 rounded-full shrink-0', statusDotColors[status])} />
|
|
<span className="font-medium truncate">{label}</span>
|
|
</div>
|
|
{sublabel && (
|
|
<div className="text-[10px] text-muted-foreground mt-0.5 truncate">{sublabel}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
if (tooltip) {
|
|
return (
|
|
<TooltipProvider delayDuration={300}>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
|
<TooltipContent side="bottom" className="max-w-64 text-xs">
|
|
{tooltip}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
}
|
|
|
|
return content;
|
|
},
|
|
);
|