e4c0d820f4
edges/index.tsx was never committed — both animation modes were rendering as snake (truthy string check). Now uses animMode to distinguish 'snake' (moving blob) from 'flow' (continuous flowing dashes).
113 lines
4.4 KiB
TypeScript
113 lines
4.4 KiB
TypeScript
import {
|
|
BaseEdge,
|
|
EdgeLabelRenderer,
|
|
getBezierPath,
|
|
getSmoothStepPath,
|
|
useStore,
|
|
type EdgeProps,
|
|
type Edge,
|
|
} from '@xyflow/react'
|
|
import type { EdgeData, EdgeType } from '@/types'
|
|
import { useThemeStore } from '@/stores/themeStore'
|
|
import { THEMES } from '@/utils/themes'
|
|
|
|
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]
|
|
}
|
|
|
|
export function HomelableEdge({ id, source, target, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data, selected }: EdgeProps<Edge<EdgeData>>) {
|
|
const activeTheme = useThemeStore((s) => s.activeTheme)
|
|
const theme = THEMES[activeTheme]
|
|
const sourceType = useStore((s) => s.nodeLookup.get(source)?.type)
|
|
const targetType = useStore((s) => s.nodeLookup.get(target)?.type)
|
|
const isBidirectional = sourceType === 'proxmox' && targetType === 'proxmox'
|
|
|
|
const pathArgs = { sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }
|
|
const [edgePath, labelX, labelY] = data?.path_style === 'smooth'
|
|
? getSmoothStepPath({ ...pathArgs, borderRadius: 8 })
|
|
: getBezierPath(pathArgs)
|
|
|
|
const edgeType: EdgeType = data?.type ?? 'ethernet'
|
|
const edgeColors = theme.colors.edgeColors
|
|
|
|
const BASE_STYLES: Record<EdgeType, React.CSSProperties> = {
|
|
ethernet: { stroke: edgeColors.ethernet, strokeWidth: 2 },
|
|
wifi: { stroke: edgeColors.wifi, strokeWidth: 1.5, strokeDasharray: '6 3' },
|
|
iot: { stroke: edgeColors.iot, strokeWidth: 1.5, strokeDasharray: '2 4' },
|
|
vlan: { strokeWidth: 2.5 },
|
|
virtual: { stroke: edgeColors.virtual, strokeWidth: 1, strokeDasharray: '4 4' },
|
|
cluster: { stroke: edgeColors.cluster, strokeWidth: 2.5, strokeDasharray: '8 3' },
|
|
}
|
|
|
|
const customColor = data?.custom_color as string | undefined
|
|
const style: React.CSSProperties = {
|
|
...BASE_STYLES[edgeType],
|
|
...(edgeType === 'vlan' ? { stroke: getVlanColor(data?.vlan_id as number | undefined) } : {}),
|
|
...(customColor ? { stroke: customColor } : {}),
|
|
...(selected ? { stroke: theme.colors.edgeSelectedColor, filter: `drop-shadow(0 0 4px ${theme.colors.edgeSelectedColor}88)` } : {}),
|
|
}
|
|
|
|
// Normalize animated value — supports legacy boolean (true → 'snake')
|
|
const animMode: 'none' | 'snake' | 'flow' =
|
|
data?.animated === true || data?.animated === 'snake' ? 'snake' :
|
|
data?.animated === 'flow' ? 'flow' : 'none'
|
|
|
|
const animColor = customColor ?? (edgeType === 'vlan' ? getVlanColor(data?.vlan_id as number | undefined) : edgeColors[edgeType as keyof typeof edgeColors] as string)
|
|
|
|
return (
|
|
<>
|
|
<BaseEdge id={id} path={edgePath} style={style} />
|
|
{animMode === 'snake' && (
|
|
<path
|
|
d={edgePath}
|
|
fill="none"
|
|
stroke={animColor}
|
|
strokeWidth={((style.strokeWidth as number ?? 2) + 1.5) * 2}
|
|
strokeDasharray="20 10000"
|
|
strokeLinecap="round"
|
|
style={{ pointerEvents: 'none' }}
|
|
>
|
|
{isBidirectional ? (
|
|
<animate attributeName="stroke-dashoffset" values="-10000;0;-10000" keyTimes="0;0.5;1" dur="20s" repeatCount="indefinite" />
|
|
) : (
|
|
<animate attributeName="stroke-dashoffset" from="-10000" to="0" dur="10s" repeatCount="indefinite" />
|
|
)}
|
|
</path>
|
|
)}
|
|
{animMode === 'flow' && (
|
|
<path
|
|
d={edgePath}
|
|
fill="none"
|
|
stroke={animColor}
|
|
strokeWidth={Math.max(3, (style.strokeWidth as number ?? 2) * 1.8)}
|
|
strokeDasharray="6 12"
|
|
strokeLinecap="round"
|
|
strokeOpacity={0.85}
|
|
style={{ pointerEvents: 'none' }}
|
|
>
|
|
<animate attributeName="stroke-dashoffset" from="0" to="18" dur="1.2s" repeatCount="indefinite" />
|
|
</path>
|
|
)}
|
|
|
|
{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: theme.colors.edgeLabelBackground,
|
|
color: theme.colors.edgeLabelColor,
|
|
border: `1px solid ${theme.colors.edgeLabelBorder}`,
|
|
}}
|
|
>
|
|
{data.label as string}
|
|
</div>
|
|
</EdgeLabelRenderer>
|
|
)}
|
|
</>
|
|
)
|
|
}
|