feat: add frontend source (fix submodule), compact node design

- Remove nested .git from Vite scaffold
- Horizontal node layout: icon left, label+IP right, reduced height
This commit is contained in:
Pouzor
2026-03-06 23:31:15 +01:00
parent 4310a5cc2d
commit 9bb34c99cc
35 changed files with 9902 additions and 1 deletions
@@ -0,0 +1,72 @@
import { useCallback } from 'react'
import {
ReactFlow,
Background,
Controls,
MiniMap,
BackgroundVariant,
type Node,
type Edge,
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import { useCanvasStore } from '@/stores/canvasStore'
import { nodeTypes } from './nodes'
import { edgeTypes } from './edges'
import type { NodeData, EdgeData } from '@/types'
export function CanvasContainer() {
const {
nodes, edges,
onNodesChange, onEdgesChange, onConnect,
setSelectedNode,
} = useCanvasStore()
const onNodeClick = useCallback((_: React.MouseEvent, node: Node<NodeData>) => {
setSelectedNode(node.id)
}, [setSelectedNode])
const onPaneClick = useCallback(() => {
setSelectedNode(null)
}, [setSelectedNode])
return (
<div className="w-full h-full">
<ReactFlow
nodes={nodes}
edges={edges as Edge<EdgeData>[]}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapToGrid
snapGrid={[16, 16]}
fitView
colorMode="dark"
>
<Background
variant={BackgroundVariant.Dots}
gap={24}
size={1}
color="#30363d"
/>
<Controls />
<MiniMap
nodeColor={(node) => {
const data = node.data as NodeData
const colorMap: Record<string, string> = {
online: '#39d353',
offline: '#f85149',
pending: '#e3b341',
unknown: '#8b949e',
}
return colorMap[data?.status ?? 'unknown']
}}
maskColor="rgba(13, 17, 23, 0.7)"
/>
</ReactFlow>
</div>
)
}
@@ -0,0 +1,63 @@
import {
BaseEdge,
EdgeLabelRenderer,
getBezierPath,
type EdgeProps,
type Edge,
} from '@xyflow/react'
import type { EdgeData, EdgeType } from '@/types'
const VLAN_COLORS = ['#00d4ff', '#a855f7', '#39d353', '#ff6e00', '#e3b341', '#f85149']
function getVlanColor(vlanId?: number): string {
if (!vlanId) return '#00d4ff'
return VLAN_COLORS[vlanId % VLAN_COLORS.length]
}
const EDGE_STYLES: Record<EdgeType, React.CSSProperties> = {
ethernet: { stroke: '#30363d', strokeWidth: 2 },
wifi: { stroke: '#00d4ff', strokeWidth: 1.5, strokeDasharray: '6 3' },
iot: { stroke: '#e3b341', strokeWidth: 1.5, strokeDasharray: '2 4' },
vlan: { strokeWidth: 2.5 },
virtual: { stroke: '#8b949e', strokeWidth: 1, strokeDasharray: '4 4' },
}
function HomelableEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data, selected }: EdgeProps<Edge<EdgeData>>) {
const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition })
const edgeType: EdgeType = data?.type ?? 'ethernet'
const style: React.CSSProperties = {
...EDGE_STYLES[edgeType],
...(edgeType === 'vlan' ? { stroke: getVlanColor(data?.vlan_id) } : {}),
...(selected ? { stroke: '#00d4ff', filter: 'drop-shadow(0 0 4px #00d4ff88)' } : {}),
}
return (
<>
<BaseEdge id={id} path={edgePath} style={style} />
{data?.label && (
<EdgeLabelRenderer>
<div
className="absolute pointer-events-none font-mono text-[10px] px-1 rounded"
style={{
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
background: '#161b22',
color: '#8b949e',
border: '1px solid #30363d',
}}
>
{data.label}
</div>
</EdgeLabelRenderer>
)}
</>
)
}
export const edgeTypes = {
ethernet: HomelableEdge,
wifi: HomelableEdge,
iot: HomelableEdge,
vlan: HomelableEdge,
virtual: HomelableEdge,
}
@@ -0,0 +1,68 @@
import { Handle, Position, type NodeProps, type Node } from '@xyflow/react'
import { type LucideIcon } from 'lucide-react'
import type { NodeData, NodeStatus } from '@/types'
const STATUS_COLORS: Record<NodeStatus, string> = {
online: '#39d353',
offline: '#f85149',
pending: '#e3b341',
unknown: '#8b949e',
}
interface BaseNodeProps extends NodeProps<Node<NodeData>> {
icon: LucideIcon
glowColor?: string
}
export function BaseNode({ data, selected, icon: Icon, glowColor = '#00d4ff' }: BaseNodeProps) {
const statusColor = STATUS_COLORS[data.status]
const isOnline = data.status === 'online'
return (
<div
className="relative flex flex-row items-center gap-2.5 px-2.5 py-2 rounded-lg border transition-all duration-200"
style={{
background: '#21262d',
borderColor: selected ? glowColor : '#30363d',
boxShadow: isOnline
? `0 0 10px ${glowColor}2e, 0 0 3px ${glowColor}1a`
: selected
? `0 0 8px ${glowColor}44`
: 'none',
opacity: data.status === 'offline' ? 0.55 : 1,
minWidth: 140,
}}
>
<Handle type="target" position={Position.Top} className="!bg-[#30363d] !border-[#8b949e]" />
{/* Icon */}
<div
className="flex items-center justify-center w-7 h-7 rounded-md shrink-0"
style={{ color: isOnline ? glowColor : '#8b949e', background: '#161b22' }}
>
<Icon size={15} />
</div>
{/* Details */}
<div className="flex flex-col min-w-0">
<div className="text-xs font-medium leading-tight truncate max-w-[110px]" title={data.label}>
{data.label}
</div>
{data.ip && (
<div className="font-mono text-[10px] text-[#8b949e] truncate" title={data.ip}>
{data.ip}
</div>
)}
</div>
{/* Status dot */}
<div
className="absolute top-1.5 right-1.5 w-1.5 h-1.5 rounded-full shrink-0"
style={{ backgroundColor: statusColor }}
title={data.status}
/>
<Handle type="source" position={Position.Bottom} className="!bg-[#30363d] !border-[#8b949e]" />
</div>
)
}
@@ -0,0 +1,35 @@
import { type NodeProps, type Node } from '@xyflow/react'
import {
Globe, Router, Network, Server, Layers, Box, Container,
HardDrive, Cpu, Wifi, Circle,
} from 'lucide-react'
import { BaseNode } from './BaseNode'
import type { NodeData } from '@/types'
type N = NodeProps<Node<NodeData>>
export const IspNode = (props: N) => <BaseNode {...props} icon={Globe} glowColor="#00d4ff" />
export const RouterNode = (props: N) => <BaseNode {...props} icon={Router} glowColor="#00d4ff" />
export const SwitchNode = (props: N) => <BaseNode {...props} icon={Network} glowColor="#39d353" />
export const ServerNode = (props: N) => <BaseNode {...props} icon={Server} glowColor="#a855f7" />
export const ProxmoxNode = (props: N) => <BaseNode {...props} icon={Layers} glowColor="#ff6e00" />
export const VmNode = (props: N) => <BaseNode {...props} icon={Box} glowColor="#a855f7" />
export const LxcNode = (props: N) => <BaseNode {...props} icon={Container} glowColor="#00d4ff" />
export const NasNode = (props: N) => <BaseNode {...props} icon={HardDrive} glowColor="#39d353" />
export const IotNode = (props: N) => <BaseNode {...props} icon={Cpu} glowColor="#e3b341" />
export const ApNode = (props: N) => <BaseNode {...props} icon={Wifi} glowColor="#00d4ff" />
export const GenericNode = (props: N) => <BaseNode {...props} icon={Circle} glowColor="#8b949e" />
export const nodeTypes = {
isp: IspNode,
router: RouterNode,
switch: SwitchNode,
server: ServerNode,
proxmox: ProxmoxNode,
vm: VmNode,
lxc: LxcNode,
nas: NasNode,
iot: IotNode,
ap: ApNode,
generic: GenericNode,
}