merge: testing → main (reconcile 2-week divergence)
This commit is contained in:
103
decnet_web/src/components/MazeNET/NodeCard.tsx
Normal file
103
decnet_web/src/components/MazeNET/NodeCard.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user