Files
DECNET/decnet_web/src/components/MazeNET/NodeCard.tsx

104 lines
3.4 KiB
TypeScript

import React from 'react';
import {
Server, Monitor, Shield, Database, Cpu, Globe, Users, HardDrive, Eye,
type LucideIcon,
} from '../../icons';
import type { MazeNode } from './types';
import { DEFAULT_SERVICES } from './data';
const ARCHETYPE_ICONS: Record<string, LucideIcon> = {
'linux-server': Server,
'windows-workstation': Monitor,
'domain-controller': Shield,
'database-server': Database,
'iot-device': Cpu,
'web-application': Globe,
'deaddeck': HardDrive,
'attacker-pool': Eye,
'directory-services': Users,
};
interface Props {
node: MazeNode;
absX: number;
absY: number;
selected: boolean;
dragging?: boolean;
deployed?: boolean;
selectedServiceSlug?: string | null;
onSelect?: (id: string) => void;
onSelectService?: (nodeId: string, slug: string) => void;
onMouseDown?: (id: string) => (e: React.MouseEvent) => void;
onPortMouseDown?: (id: string) => (e: React.MouseEvent) => void;
onContextMenu?: (id: string) => (e: React.MouseEvent) => void;
}
const NodeCard: React.FC<Props> = ({ node, absX, absY, selected, dragging, deployed, selectedServiceSlug, onSelect, onSelectService, onMouseDown, onPortMouseDown, onContextMenu }) => {
const isDmzGateway = !!(node as { decky_config?: { forwards_l3?: boolean } }).decky_config?.forwards_l3;
const classes = [
'maze-node',
node.kind === 'observed' ? 'observed' : '',
node.status === 'hot' ? 'hot' : '',
selected ? 'selected' : '',
dragging ? 'dragging' : '',
deployed ? 'deployed' : '',
deployed && isDmzGateway ? 'dmz-gateway' : '',
].filter(Boolean).join(' ');
const handleDown = (e: React.MouseEvent) => {
onSelect?.(node.id);
onMouseDown?.(node.id)(e);
};
return (
<div
className={classes}
style={{ left: absX, top: absY }}
onMouseDown={handleDown}
onContextMenu={onContextMenu?.(node.id)}
>
<div className="mn-head">
<span className={`status-dot ${node.status}`} />
{(() => {
const Icon = ARCHETYPE_ICONS[node.archetype] ?? Server;
return <Icon size={10} className="mn-head-icon" />;
})()}
<span className="mn-head-name">{node.name}</span>
</div>
<div className="mn-sub">{node.archetype.toUpperCase()}</div>
{node.services.length > 0 && (
<div className="mn-services">
{node.services.map((s) => {
const meta = DEFAULT_SERVICES.find((x) => x.slug === s);
const isHigh = meta?.risk === 'high' || node.status === 'hot';
const isSel = selectedServiceSlug === s;
return (
<span
key={s}
className={`service-tag ${isHigh ? 'hot' : ''} ${isSel ? 'service-selected' : ''}`}
title={meta ? `${meta.name} · ${meta.proto.toUpperCase()}:${meta.port}` : s}
onMouseDown={(e) => {
if (!onSelectService) return;
e.stopPropagation();
onSelectService(node.id, s);
}}
>
{s}
</span>
);
})}
</div>
)}
{node.kind === 'decky' && <>
<span className="mn-port in" />
<span className="mn-port out" onMouseDown={onPortMouseDown?.(node.id)} />
</>}
{node.kind === 'observed' && (
<span className="mn-port out" onMouseDown={onPortMouseDown?.(node.id)} />
)}
</div>
);
};
export default React.memo(NodeCard);