Files
rack-planner/client/src/components/mapper/nodes/DeviceNode.tsx
T

77 lines
2.8 KiB
TypeScript

import { memo, useMemo } from 'react';
import { Handle, Position, type NodeProps } from '@xyflow/react';
import type { Module } from '../../../types';
import { MODULE_TYPE_COLORS, MODULE_TYPE_LABELS } from '../../../lib/constants';
import { Badge } from '../../ui/Badge';
export interface DeviceNodeData {
label: string;
module?: Module;
[key: string]: unknown;
}
export const DeviceNode = memo(({ data, selected }: NodeProps) => {
const nodeData = data as DeviceNodeData;
const mod = nodeData.module;
const colors = mod ? MODULE_TYPE_COLORS[mod.type] : null;
const meta = useMemo(() => {
try {
return nodeData.metadata ? JSON.parse(nodeData.metadata as string) : {};
} catch {
return {};
}
}, [nodeData.metadata]);
const ipToDisplay = meta.ipAddress || mod?.ipAddress;
const hasAddress = ipToDisplay || meta.port;
return (
<div
className={`min-w-[160px] max-w-[200px] bg-slate-800 border rounded-lg shadow-lg overflow-hidden transition-all ${
selected ? 'ring-2 ring-blue-500 border-blue-500' : 'border-slate-600'
} ${colors ? colors.border : ''}`}
>
<Handle type="target" position={Position.Top} className="!bg-slate-400 !border-slate-600" />
{/* Colored accent strip */}
{colors && <div className={`h-1 w-full ${colors.bg}`} />}
<div className="px-3 py-2">
<div className="flex items-center gap-1.5 mb-1">
<svg width="12" height="12" viewBox="0 0 18 18" fill="none" className="shrink-0 opacity-60">
<rect x="1" y="2" width="16" height="3" rx="1" fill="currentColor" />
<rect x="1" y="7" width="16" height="3" rx="1" fill="currentColor" opacity="0.7" />
<rect x="1" y="12" width="16" height="3" rx="1" fill="currentColor" opacity="0.4" />
</svg>
<span className="text-xs font-semibold text-slate-100 truncate">{nodeData.label}</span>
</div>
{mod && (
<div className="flex flex-wrap gap-1 mt-1">
<Badge variant="slate" className="text-[10px]">{MODULE_TYPE_LABELS[mod.type]}</Badge>
{hasAddress && (
<span className="text-[10px] text-slate-400 font-mono ml-1 mt-0.5">
{ipToDisplay}{meta.port ? `:${meta.port}` : ''}
</span>
)}
</div>
)}
{!mod && (
<div className="flex flex-col mt-1">
<span className="text-[10px] text-slate-500">Unlinked device</span>
{hasAddress && (
<span className="text-[10px] text-slate-400 font-mono">
{ipToDisplay}{meta.port ? `:${meta.port}` : ''}
</span>
)}
</div>
)}
</div>
<Handle type="source" position={Position.Bottom} className="!bg-slate-400 !border-slate-600" />
</div>
);
});
DeviceNode.displayName = 'DeviceNode';