bdf3b6ea40
- Ctrl+F / Cmd+F opens floating search bar at top-center of canvas - Filters nodes by label, IP, hostname and service name (case-insensitive) - Shows match count and no-results message - Click result selects node and flies camera to it with animation - Escape or × closes the bar - groupRect nodes excluded from results - Handles grouped nodes with correct absolute position for navigation
105 lines
3.3 KiB
TypeScript
105 lines
3.3 KiB
TypeScript
import { useCallback, useState } from 'react'
|
|
import {
|
|
ReactFlow,
|
|
Background,
|
|
Controls,
|
|
ControlButton,
|
|
BackgroundVariant,
|
|
ConnectionMode,
|
|
SelectionMode,
|
|
type Node,
|
|
type Edge,
|
|
type Connection,
|
|
} from '@xyflow/react'
|
|
import { MousePointer2, Hand } from 'lucide-react'
|
|
import '@xyflow/react/dist/style.css'
|
|
import { useCanvasStore } from '@/stores/canvasStore'
|
|
import { useThemeStore } from '@/stores/themeStore'
|
|
import { THEMES } from '@/utils/themes'
|
|
import { nodeTypes } from './nodes/nodeTypes'
|
|
import { edgeTypes } from './edges/edgeTypes'
|
|
import { SearchBar } from './SearchBar'
|
|
import type { NodeData, EdgeData } from '@/types'
|
|
|
|
interface CanvasContainerProps {
|
|
onConnect?: (connection: Connection) => void
|
|
onEdgeDoubleClick?: (edge: Edge<EdgeData>) => void
|
|
onNodeDragStart?: () => void
|
|
}
|
|
|
|
export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, onNodeDragStart }: CanvasContainerProps) {
|
|
const [lassoMode, setLassoMode] = useState(true)
|
|
const {
|
|
nodes, edges,
|
|
onNodesChange, onEdgesChange,
|
|
setSelectedNode, snapshotHistory,
|
|
} = useCanvasStore()
|
|
|
|
const activeTheme = useThemeStore((s) => s.activeTheme)
|
|
const theme = THEMES[activeTheme]
|
|
|
|
const onNodeClick = useCallback((e: React.MouseEvent, node: Node<NodeData>) => {
|
|
if (e.ctrlKey || e.metaKey) {
|
|
setSelectedNode(null)
|
|
} else {
|
|
setSelectedNode(node.id)
|
|
}
|
|
}, [setSelectedNode])
|
|
|
|
const onPaneClick = useCallback(() => {
|
|
setSelectedNode(null)
|
|
}, [setSelectedNode])
|
|
|
|
const handleEdgeDoubleClick = useCallback((_: React.MouseEvent, edge: Edge<EdgeData>) => {
|
|
onEdgeDoubleClick?.(edge)
|
|
}, [onEdgeDoubleClick])
|
|
|
|
return (
|
|
<div className="w-full h-full" style={{ background: theme.colors.canvasBackground }}>
|
|
<ReactFlow
|
|
nodes={nodes}
|
|
edges={edges}
|
|
onNodesChange={onNodesChange}
|
|
onEdgesChange={onEdgesChange}
|
|
onConnect={onConnectProp}
|
|
onNodeClick={onNodeClick}
|
|
onPaneClick={onPaneClick}
|
|
onEdgeDoubleClick={handleEdgeDoubleClick}
|
|
onNodeDragStart={onNodeDragStart}
|
|
nodeTypes={nodeTypes}
|
|
edgeTypes={edgeTypes}
|
|
deleteKeyCode={['Backspace', 'Delete']}
|
|
onBeforeDelete={async () => { snapshotHistory(); return true }}
|
|
selectionOnDrag={lassoMode}
|
|
panOnDrag={lassoMode ? [1, 2] : true}
|
|
panActivationKeyCode="Space"
|
|
selectionMode={SelectionMode.Partial}
|
|
multiSelectionKeyCode={['Meta', 'Control']}
|
|
snapToGrid
|
|
snapGrid={[16, 16]}
|
|
fitView
|
|
colorMode={theme.colors.reactFlowColorMode}
|
|
elevateNodesOnSelect={false}
|
|
connectionMode={ConnectionMode.Loose}
|
|
isValidConnection={(connection) => connection.source !== connection.target}
|
|
>
|
|
<Background
|
|
variant={BackgroundVariant.Dots}
|
|
gap={24}
|
|
size={1}
|
|
color={theme.colors.canvasDotColor}
|
|
/>
|
|
<SearchBar />
|
|
<Controls>
|
|
<ControlButton
|
|
onClick={() => setLassoMode((m) => !m)}
|
|
title={lassoMode ? 'Switch to pan mode (Space to pan)' : 'Switch to lasso mode'}
|
|
>
|
|
{lassoMode ? <MousePointer2 size={12} /> : <Hand size={12} />}
|
|
</ControlButton>
|
|
</Controls>
|
|
</ReactFlow>
|
|
</div>
|
|
)
|
|
}
|