diff --git a/backend/app/api/routes/canvas.py b/backend/app/api/routes/canvas.py index 4b0de28..cdfdd9b 100644 --- a/backend/app/api/routes/canvas.py +++ b/backend/app/api/routes/canvas.py @@ -23,12 +23,40 @@ async def load_canvas(db: AsyncSession = Depends(get_db), _: str = Depends(get_c @router.post("/save") async def save_canvas(body: CanvasSaveRequest, db: AsyncSession = Depends(get_db), _: str = Depends(get_current_user)): - # Update node positions from canvas - for node_pos in body.node_positions: - node = await db.get(Node, node_pos.id) + incoming_node_ids = {n.id for n in body.nodes} + incoming_edge_ids = {e.id for e in body.edges} + + # Delete nodes removed from canvas + existing_nodes = (await db.execute(select(Node))).scalars().all() + for node in existing_nodes: + if node.id not in incoming_node_ids: + await db.delete(node) + + # Delete edges removed from canvas + existing_edges = (await db.execute(select(Edge))).scalars().all() + for edge in existing_edges: + if edge.id not in incoming_edge_ids: + await db.delete(edge) + + await db.flush() + + # Upsert nodes + for node_data in body.nodes: + node = await db.get(Node, node_data.id) if node: - node.pos_x = node_pos.x - node.pos_y = node_pos.y + for field, value in node_data.model_dump().items(): + setattr(node, field, value) + else: + db.add(Node(**node_data.model_dump())) + + # Upsert edges + for edge_data in body.edges: + edge = await db.get(Edge, edge_data.id) + if edge: + for field, value in edge_data.model_dump().items(): + setattr(edge, field, value) + else: + db.add(Edge(**edge_data.model_dump())) # Upsert viewport state = await db.get(CanvasState, 1) diff --git a/backend/app/schemas/canvas.py b/backend/app/schemas/canvas.py index 7062289..ee503f3 100644 --- a/backend/app/schemas/canvas.py +++ b/backend/app/schemas/canvas.py @@ -1,17 +1,42 @@ +from typing import Any + from pydantic import BaseModel from app.schemas.edges import EdgeResponse from app.schemas.nodes import NodeResponse -class NodePosition(BaseModel): +class NodeSave(BaseModel): id: str - x: float - y: float + type: str + label: str + hostname: str | None = None + ip: str | None = None + mac: str | None = None + os: str | None = None + status: str = "unknown" + check_method: str | None = None + check_target: str | None = None + services: list[Any] = [] + notes: str | None = None + parent_id: str | None = None + pos_x: float = 0 + pos_y: float = 0 + + +class EdgeSave(BaseModel): + id: str + source: str + target: str + type: str = "ethernet" + label: str | None = None + vlan_id: int | None = None + speed: str | None = None class CanvasSaveRequest(BaseModel): - node_positions: list[NodePosition] = [] + nodes: list[NodeSave] = [] + edges: list[EdgeSave] = [] viewport: dict = {} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f1e0d28..ff4b699 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -36,15 +36,39 @@ export default function App() { // Declare handleSave before the Ctrl+S effect so it is in scope const handleSave = useCallback(async () => { try { - const nodePositions = nodes.map((n) => ({ id: n.id, x: n.position.x, y: n.position.y })) - await canvasApi.save({ node_positions: nodePositions, viewport: {} }) + const nodesToSave = nodes.map((n) => ({ + id: n.id, + type: n.data.type, + label: n.data.label, + hostname: n.data.hostname ?? null, + ip: n.data.ip ?? null, + mac: n.data.mac ?? null, + os: n.data.os ?? null, + status: n.data.status, + check_method: n.data.check_method ?? null, + check_target: n.data.check_target ?? null, + services: n.data.services ?? [], + notes: n.data.notes ?? null, + parent_id: n.data.parent_id ?? null, + pos_x: n.position.x, + pos_y: n.position.y, + })) + const edgesToSave = edges.map((e) => ({ + id: e.id, + source: e.source, + target: e.target, + type: e.data?.type ?? 'ethernet', + label: e.data?.label ?? null, + vlan_id: e.data?.vlan_id ?? null, + speed: e.data?.speed ?? null, + })) + await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport: {} }) markSaved() toast.success('Canvas saved') } catch { - markSaved() - toast.success('Canvas saved (local)') + toast.error('Save failed') } - }, [nodes, markSaved]) + }, [nodes, edges, markSaved]) // Keep a ref so the keydown handler always calls the latest version const handleSaveRef = useRef(handleSave) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 90defed..e8d1145 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -26,8 +26,11 @@ export const authApi = { export const canvasApi = { load: () => api.get('/canvas'), - save: (payload: { node_positions: { id: string; x: number; y: number }[]; viewport: object }) => - api.post('/canvas/save', payload), + save: (payload: { + nodes: object[] + edges: object[] + viewport: object + }) => api.post('/canvas/save', payload), } export const nodesApi = {