147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
import { useCallback } from 'react'
|
|
import {
|
|
ReactFlow,
|
|
Background,
|
|
Controls,
|
|
MiniMap,
|
|
BackgroundVariant,
|
|
type OnConnect,
|
|
type OnReconnect,
|
|
type OnNodesChange,
|
|
type OnEdgesChange,
|
|
type Node,
|
|
type Edge,
|
|
} from '@xyflow/react'
|
|
import { nodeTypes } from './nodes/nodeTypes'
|
|
import { edgeTypes } from './edges/edgeTypes'
|
|
import { getDeviceRenderConfig } from './nodes/deviceRegistry'
|
|
import type { DeviceNodeData } from './nodes/DeviceNode'
|
|
import type { InteractionMode } from './DiagramHeader'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface NetworkCanvasProps {
|
|
nodes: Node[]
|
|
edges: Edge[]
|
|
onNodesChange: OnNodesChange
|
|
onEdgesChange: OnEdgesChange
|
|
onConnect: OnConnect
|
|
onReconnect: OnReconnect<Edge>
|
|
onNodeSelect: (nodeId: string | null) => void
|
|
onEdgeSelect: (edgeId: string | null) => void
|
|
onDrop: (event: React.DragEvent) => void
|
|
onDragOver: (event: React.DragEvent) => void
|
|
onDragLeave?: (event: React.DragEvent) => void
|
|
isDragOver?: boolean
|
|
onNodeContextMenu?: (event: React.MouseEvent, node: Node) => void
|
|
onPaneContextMenu?: (event: MouseEvent | React.MouseEvent) => void
|
|
onPaneClick?: () => void
|
|
interactionMode?: InteractionMode
|
|
}
|
|
|
|
export function NetworkCanvas({
|
|
nodes,
|
|
edges,
|
|
onNodesChange,
|
|
onEdgesChange,
|
|
onConnect,
|
|
onReconnect,
|
|
onNodeSelect,
|
|
onEdgeSelect,
|
|
onDrop,
|
|
onDragOver,
|
|
onDragLeave,
|
|
isDragOver,
|
|
onNodeContextMenu,
|
|
onPaneContextMenu,
|
|
onPaneClick: onPaneClickProp,
|
|
interactionMode = 'select',
|
|
}: NetworkCanvasProps) {
|
|
const handleSelectionChange = useCallback(({ nodes: selectedNodes, edges: selectedEdges }: { nodes: Node[]; edges: Edge[] }) => {
|
|
if (selectedNodes.length === 1) {
|
|
onNodeSelect(selectedNodes[0].id)
|
|
onEdgeSelect(null)
|
|
} else if (selectedEdges.length === 1) {
|
|
onEdgeSelect(selectedEdges[0].id)
|
|
onNodeSelect(null)
|
|
} else {
|
|
onNodeSelect(null)
|
|
onEdgeSelect(null)
|
|
}
|
|
}, [onNodeSelect, onEdgeSelect])
|
|
|
|
const handlePaneClick = useCallback(() => {
|
|
onNodeSelect(null)
|
|
onEdgeSelect(null)
|
|
onPaneClickProp?.()
|
|
}, [onNodeSelect, onEdgeSelect, onPaneClickProp])
|
|
|
|
const getNodeColor = useCallback((node: Node) => {
|
|
if (node.type === 'group') return 'var(--color-bg-elevated)'
|
|
const data = node.data as unknown as DeviceNodeData
|
|
return getDeviceRenderConfig(data?.deviceType || '', data?.category).color
|
|
}, [])
|
|
|
|
return (
|
|
<div
|
|
className="relative h-full w-full"
|
|
onDragLeave={onDragLeave}
|
|
onMouseDownCapture={(event) => {
|
|
if (event.button === 1) {
|
|
event.preventDefault()
|
|
}
|
|
}}
|
|
>
|
|
<ReactFlow
|
|
nodes={nodes}
|
|
edges={edges}
|
|
onNodesChange={onNodesChange}
|
|
onEdgesChange={onEdgesChange}
|
|
onConnect={onConnect}
|
|
onReconnect={onReconnect}
|
|
onSelectionChange={handleSelectionChange}
|
|
onPaneClick={handlePaneClick}
|
|
onDrop={onDrop}
|
|
onDragOver={onDragOver}
|
|
onNodeContextMenu={onNodeContextMenu}
|
|
onPaneContextMenu={onPaneContextMenu}
|
|
nodeTypes={nodeTypes}
|
|
edgeTypes={edgeTypes}
|
|
defaultEdgeOptions={{ type: 'connection' }}
|
|
edgesReconnectable
|
|
connectOnClick={interactionMode === 'connect'}
|
|
reconnectRadius={20}
|
|
connectionRadius={24}
|
|
deleteKeyCode={['Backspace', 'Delete']}
|
|
multiSelectionKeyCode="Shift"
|
|
panOnDrag={interactionMode === 'pan' ? [0, 1] : [1]}
|
|
selectionOnDrag={interactionMode === 'select'}
|
|
panActivationKeyCode="Space"
|
|
snapToGrid={true}
|
|
snapGrid={[20, 20]}
|
|
fitView
|
|
className={cn(
|
|
'bg-page',
|
|
interactionMode === 'pan' && 'cursor-grab active:cursor-grabbing',
|
|
interactionMode === 'connect' && 'rf-connect-mode cursor-crosshair',
|
|
)}
|
|
>
|
|
<Background variant={BackgroundVariant.Dots} color="var(--color-border-default)" gap={20} size={1} />
|
|
<Controls className="!border-default !bg-card [&>button]:!border-default [&>button]:!bg-card [&>button]:!fill-text-primary" />
|
|
<MiniMap
|
|
nodeColor={getNodeColor}
|
|
maskColor="rgba(0,0,0,0.5)"
|
|
className="!border-default !bg-card"
|
|
position="bottom-right"
|
|
/>
|
|
</ReactFlow>
|
|
{isDragOver && (
|
|
<div className="pointer-events-none absolute inset-2 z-10 flex items-center justify-center rounded-lg border-2 border-dashed border-accent/30">
|
|
<span className="rounded-md bg-card/80 px-3 py-1.5 text-sm text-muted-foreground">
|
|
Drop to add
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|