fix: network diagram team_id guard + multi-style edge routing

Backend:
- Guard create_diagram with 422 if current_user.team_id is None (prevents
  NOT NULL constraint crash for accounts not yet assigned to a team)
- Add routing field to DiagramEdge schema (straight/curved/step)

Frontend:
- ConnectionEdge now supports straight (default), curved (bezier), and
  step (smooth-step) routing per-edge via routing field in edge data
- PropertiesPanel Connection section gets a Line Style toggle:
  Straight | Curved | Step buttons, active state highlights in accent
- handleEdgeUpdate and serializeEdges now propagate the routing field
- DiagramEdge type gets optional routing field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-04-05 00:05:12 +00:00
parent b490719667
commit 0dc2801916
6 changed files with 69 additions and 17 deletions

View File

@@ -130,6 +130,11 @@ async def create_diagram(
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_active_user)],
) -> NetworkDiagramResponse:
if current_user.team_id is None:
raise HTTPException(
status_code=422,
detail="Network Diagrams require a team account. Assign your account to a team first.",
)
diagram = NetworkDiagram(
team_id=current_user.team_id,
name=data.name,

View File

@@ -38,6 +38,7 @@ class DiagramEdge(BaseModel):
connectionType: str = "ethernet"
speed: str | None = None
notes: str | None = None
routing: str | None = None
class NetworkDiagramCreate(BaseModel):

View File

@@ -1,8 +1,9 @@
import { memo } from 'react'
import { BaseEdge, EdgeLabelRenderer, getStraightPath, type EdgeProps } from '@xyflow/react'
import { BaseEdge, EdgeLabelRenderer, getStraightPath, getBezierPath, getSmoothStepPath, type EdgeProps } from '@xyflow/react'
interface ConnectionEdgeData {
connectionType?: string
routing?: string | null
speed?: string | null
notes?: string | null
[key: string]: unknown
@@ -19,17 +20,26 @@ const CONNECTION_STYLES: Record<string, { stroke: string; strokeDasharray?: stri
const DEFAULT_STYLE = { stroke: '#848b9b', strokeWidth: 2 }
function getEdgePath(routing: string | null | undefined, props: EdgeProps) {
const base = {
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
}
if (routing === 'curved') return getBezierPath(base)
if (routing === 'step') return getSmoothStepPath(base)
return getStraightPath(base)
}
function ConnectionEdgeComponent(props: EdgeProps) {
const edgeData = props.data as ConnectionEdgeData | undefined
const connectionType = edgeData?.connectionType || 'ethernet'
const style = CONNECTION_STYLES[connectionType] || DEFAULT_STYLE
const [edgePath, labelX, labelY] = getStraightPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
})
const [edgePath, labelX, labelY] = getEdgePath(edgeData?.routing, props)
return (
<>

View File

@@ -1,5 +1,5 @@
import { useCallback } from 'react'
import { Trash2 } from 'lucide-react'
import { Trash2, Minus, Spline, GitBranch } from 'lucide-react'
import { cn } from '@/lib/utils'
import type { DeviceProperties, DiagramEdge } from '@/types'
import type { Node, Edge } from '@xyflow/react'
@@ -139,6 +139,35 @@ export function PropertiesPanel({
mono
/>
</div>
<div className="flex flex-col gap-1">
<FieldLabel>Line Style</FieldLabel>
<div className="flex gap-1">
{([
{ value: null, icon: Minus, label: 'Straight' },
{ value: 'curved', icon: Spline, label: 'Curved' },
{ value: 'step', icon: GitBranch, label: 'Step' },
] as const).map(({ value, icon: Icon, label }) => {
const routing = (edgeData.routing as string | null | undefined) ?? null
const active = routing === value
return (
<button
key={label}
title={label}
onClick={() => onEdgeUpdate(selectedEdge.id, { routing: value })}
className={cn(
'flex flex-1 items-center justify-center gap-1 rounded border py-1.5 text-[10px] transition-colors',
active
? 'border-accent bg-accent/10 text-accent'
: 'border-default text-muted-foreground hover:border-hover hover:text-primary',
)}
>
<Icon size={12} />
{label}
</button>
)
})}
</div>
</div>
<div className="flex items-center justify-between">
<FieldLabel>Show Traffic</FieldLabel>
<button

View File

@@ -151,6 +151,7 @@ function DiagramEditorInner() {
connectionType: e.connectionType,
speed: e.speed,
notes: e.notes,
routing: e.routing ?? null,
},
}))
)
@@ -193,15 +194,19 @@ function DiagramEditorInner() {
}, [getNodes])
const serializeEdges = useCallback((): DiagramEdge[] => {
return edges.map(e => ({
id: e.id,
source: e.source,
target: e.target,
label: (e.label as string) || null,
connectionType: (e.data as Record<string, unknown>)?.connectionType as string || 'ethernet',
speed: (e.data as Record<string, unknown>)?.speed as string || null,
notes: (e.data as Record<string, unknown>)?.notes as string || null,
}))
return edges.map(e => {
const d = (e.data as Record<string, unknown>) || {}
return {
id: e.id,
source: e.source,
target: e.target,
label: (e.label as string) || null,
connectionType: d.connectionType as string || 'ethernet',
speed: d.speed as string || null,
notes: d.notes as string || null,
routing: d.routing as string || null,
}
})
}, [edges])
const handleSave = useCallback(async () => {
@@ -363,6 +368,7 @@ function DiagramEditorInner() {
...(updates.connectionType !== undefined ? { connectionType: updates.connectionType } : {}),
...(updates.speed !== undefined ? { speed: updates.speed } : {}),
...(updates.notes !== undefined ? { notes: updates.notes } : {}),
...(updates.routing !== undefined ? { routing: updates.routing } : {}),
},
}
}))

View File

@@ -28,6 +28,7 @@ export interface DiagramEdge {
connectionType: string
speed: string | null
notes: string | null
routing?: string | null
}
export interface DeviceTypeResponse {