Advanced pages. Model-driven controls.
This commit is contained in:
91
web/src/components/domains/TopologyNode.tsx
Normal file
91
web/src/components/domains/TopologyNode.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user