77 lines
2.8 KiB
TypeScript
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';
|