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:
@@ -130,6 +130,11 @@ async def create_diagram(
|
|||||||
db: Annotated[AsyncSession, Depends(get_db)],
|
db: Annotated[AsyncSession, Depends(get_db)],
|
||||||
current_user: Annotated[User, Depends(get_current_active_user)],
|
current_user: Annotated[User, Depends(get_current_active_user)],
|
||||||
) -> NetworkDiagramResponse:
|
) -> 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(
|
diagram = NetworkDiagram(
|
||||||
team_id=current_user.team_id,
|
team_id=current_user.team_id,
|
||||||
name=data.name,
|
name=data.name,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class DiagramEdge(BaseModel):
|
|||||||
connectionType: str = "ethernet"
|
connectionType: str = "ethernet"
|
||||||
speed: str | None = None
|
speed: str | None = None
|
||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
|
routing: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class NetworkDiagramCreate(BaseModel):
|
class NetworkDiagramCreate(BaseModel):
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { memo } from 'react'
|
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 {
|
interface ConnectionEdgeData {
|
||||||
connectionType?: string
|
connectionType?: string
|
||||||
|
routing?: string | null
|
||||||
speed?: string | null
|
speed?: string | null
|
||||||
notes?: string | null
|
notes?: string | null
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
@@ -19,17 +20,26 @@ const CONNECTION_STYLES: Record<string, { stroke: string; strokeDasharray?: stri
|
|||||||
|
|
||||||
const DEFAULT_STYLE = { stroke: '#848b9b', strokeWidth: 2 }
|
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) {
|
function ConnectionEdgeComponent(props: EdgeProps) {
|
||||||
const edgeData = props.data as ConnectionEdgeData | undefined
|
const edgeData = props.data as ConnectionEdgeData | undefined
|
||||||
const connectionType = edgeData?.connectionType || 'ethernet'
|
const connectionType = edgeData?.connectionType || 'ethernet'
|
||||||
const style = CONNECTION_STYLES[connectionType] || DEFAULT_STYLE
|
const style = CONNECTION_STYLES[connectionType] || DEFAULT_STYLE
|
||||||
|
|
||||||
const [edgePath, labelX, labelY] = getStraightPath({
|
const [edgePath, labelX, labelY] = getEdgePath(edgeData?.routing, props)
|
||||||
sourceX: props.sourceX,
|
|
||||||
sourceY: props.sourceY,
|
|
||||||
targetX: props.targetX,
|
|
||||||
targetY: props.targetY,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { Trash2 } from 'lucide-react'
|
import { Trash2, Minus, Spline, GitBranch } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import type { DeviceProperties, DiagramEdge } from '@/types'
|
import type { DeviceProperties, DiagramEdge } from '@/types'
|
||||||
import type { Node, Edge } from '@xyflow/react'
|
import type { Node, Edge } from '@xyflow/react'
|
||||||
@@ -139,6 +139,35 @@ export function PropertiesPanel({
|
|||||||
mono
|
mono
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="flex items-center justify-between">
|
||||||
<FieldLabel>Show Traffic</FieldLabel>
|
<FieldLabel>Show Traffic</FieldLabel>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ function DiagramEditorInner() {
|
|||||||
connectionType: e.connectionType,
|
connectionType: e.connectionType,
|
||||||
speed: e.speed,
|
speed: e.speed,
|
||||||
notes: e.notes,
|
notes: e.notes,
|
||||||
|
routing: e.routing ?? null,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
@@ -193,15 +194,19 @@ function DiagramEditorInner() {
|
|||||||
}, [getNodes])
|
}, [getNodes])
|
||||||
|
|
||||||
const serializeEdges = useCallback((): DiagramEdge[] => {
|
const serializeEdges = useCallback((): DiagramEdge[] => {
|
||||||
return edges.map(e => ({
|
return edges.map(e => {
|
||||||
id: e.id,
|
const d = (e.data as Record<string, unknown>) || {}
|
||||||
source: e.source,
|
return {
|
||||||
target: e.target,
|
id: e.id,
|
||||||
label: (e.label as string) || null,
|
source: e.source,
|
||||||
connectionType: (e.data as Record<string, unknown>)?.connectionType as string || 'ethernet',
|
target: e.target,
|
||||||
speed: (e.data as Record<string, unknown>)?.speed as string || null,
|
label: (e.label as string) || null,
|
||||||
notes: (e.data as Record<string, unknown>)?.notes 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])
|
}, [edges])
|
||||||
|
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
@@ -363,6 +368,7 @@ function DiagramEditorInner() {
|
|||||||
...(updates.connectionType !== undefined ? { connectionType: updates.connectionType } : {}),
|
...(updates.connectionType !== undefined ? { connectionType: updates.connectionType } : {}),
|
||||||
...(updates.speed !== undefined ? { speed: updates.speed } : {}),
|
...(updates.speed !== undefined ? { speed: updates.speed } : {}),
|
||||||
...(updates.notes !== undefined ? { notes: updates.notes } : {}),
|
...(updates.notes !== undefined ? { notes: updates.notes } : {}),
|
||||||
|
...(updates.routing !== undefined ? { routing: updates.routing } : {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface DiagramEdge {
|
|||||||
connectionType: string
|
connectionType: string
|
||||||
speed: string | null
|
speed: string | null
|
||||||
notes: string | null
|
notes: string | null
|
||||||
|
routing?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceTypeResponse {
|
export interface DeviceTypeResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user