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)],
|
||||
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,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface DiagramEdge {
|
||||
connectionType: string
|
||||
speed: string | null
|
||||
notes: string | null
|
||||
routing?: string | null
|
||||
}
|
||||
|
||||
export interface DeviceTypeResponse {
|
||||
|
||||
Reference in New Issue
Block a user