diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 52df594..19e2d0a 100644 Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/diagrams/diagram_argo.json b/backend/diagrams/diagram_argo.json new file mode 100644 index 0000000..184c8c0 --- /dev/null +++ b/backend/diagrams/diagram_argo.json @@ -0,0 +1,19 @@ +{ + "nodes": [ + { + "id": "1", + "type": "custom", + "data": { + "label": "Node 1", + "icon": "https://s3.dvirlabs.com/lab-icons/dev-tools/argocd.svg" + }, + "position": { + "x": 307.58165778037267, + "y": 195.1586839847007 + }, + "width": 103, + "height": 103 + } + ], + "edges": [] +} \ No newline at end of file diff --git a/backend/diagrams/diagram_infra.json b/backend/diagrams/diagram_infra.json new file mode 100644 index 0000000..e93e127 --- /dev/null +++ b/backend/diagrams/diagram_infra.json @@ -0,0 +1,19 @@ +{ + "nodes": [ + { + "id": "1", + "type": "custom", + "data": { + "label": "Node 1", + "icon": "https://s3.dvirlabs.com/lab-icons/infra/cloudflare.svg" + }, + "position": { + "x": 19.293138020977764, + "y": 245.65982158849926 + }, + "width": 103, + "height": 103 + } + ], + "edges": [] +} \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index b2ff2e4..4b7b361 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,10 +3,10 @@ from fastapi.middleware.cors import CORSMiddleware import uvicorn from models import DiagramItem import json -import os import requests import xml.etree.ElementTree as ET from typing import List, Dict +from pathlib import Path app = FastAPI() @@ -17,31 +17,54 @@ app.add_middleware( allow_headers=["*"], ) +# Static icon info BASE_URL = "https://s3.dvirlabs.com/lab-icons" S3_INDEX_URL = "https://s3.dvirlabs.com/lab-icons/?list-type=2" -DATA_FILE = "diagram.json" + +# Directory for storing diagrams +BASE_DIR = Path(__file__).parent.resolve() +DATA_DIR = BASE_DIR / "diagrams" +DATA_DIR.mkdir(exist_ok=True) @app.get("/") def root(): return {"message": "Check if the server is running"} @app.get("/diagram/fetch") -def fetch_diagram(): - if not os.path.exists(DATA_FILE): +def fetch_diagram(name: str): + path = DATA_DIR / f"diagram_{name}.json" + if not path.exists(): return {"nodes": [], "edges": []} - with open(DATA_FILE, "r") as f: + with open(path, "r") as f: return json.load(f) - @app.post("/diagram/save") -def save_diagram(payload: DiagramItem): +def save_diagram(name: str, payload: DiagramItem): try: - with open(DATA_FILE, "w") as f: + path = DATA_DIR / f"diagram_{name}.json" + with open(path, "w") as f: json.dump(payload.dict(), f, indent=2) return {"status": "ok"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) +@app.get("/diagram/list") +def list_diagrams(): + diagrams = [] + for file in DATA_DIR.glob("diagram_*.json"): + diagrams.append(file.stem.replace("diagram_", "")) + return {"diagrams": diagrams} + +@app.delete("/diagram/delete") +def delete_diagram(name: str): + path = DATA_DIR / f"diagram_{name}.json" + if not path.exists(): + raise HTTPException(status_code=404, detail="Diagram not found") + try: + path.unlink() + return {"status": "deleted"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) @app.get("/icons", response_model=Dict[str, List[str]]) def list_icons(): @@ -79,10 +102,5 @@ def list_icons(): return categories - -# if __name__ == "__main__": -# uvicorn.run(app, host="0.0.0.0", port=8000) - -# For development, you can uncomment the line below to run the server with uvicorn directly. if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8001) \ No newline at end of file + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/diagram.json b/diagram.json deleted file mode 100644 index 4736f13..0000000 --- a/diagram.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "nodes": [], - "edges": [] -} \ No newline at end of file diff --git a/frontend/src/components/Diagram.jsx b/frontend/src/components/Diagram.jsx index 3a97dd1..1e3dd7e 100644 --- a/frontend/src/components/Diagram.jsx +++ b/frontend/src/components/Diagram.jsx @@ -7,7 +7,12 @@ import ReactFlow, { useEdgesState, } from 'reactflow'; import 'reactflow/dist/style.css'; -import { fetchDiagram, saveDiagram } from '../services/api'; +import { + fetchDiagramByName, + saveDiagramByName, + listDiagrams, + deleteDiagramByName, +} from '../services/api'; import CustomNode from './CustomNode'; import IconSelector from './IconSelector'; import { toast } from 'react-toastify'; @@ -24,30 +29,84 @@ function Diagram() { const [newLabel, setNewLabel] = useState(''); const [selectedIcon, setSelectedIcon] = useState(''); const [selectedEdge, setSelectedEdge] = useState(null); - + const [diagramName, setDiagramName] = useState(null); + const [diagramList, setDiagramList] = useState([]); useEffect(() => { - fetchDiagram().then((data) => { - setNodes(data.nodes || []); - const sanitizedEdges = (data.edges || []).map(({ id, source, target, animated }) => ({ - id, - source, - target, - animated: !!animated, - })); - setEdges(sanitizedEdges); - }); - }, [setNodes, setEdges]); + let isMounted = true; + + const load = async () => { + try { + const { diagrams } = await listDiagrams(); + if (isMounted) { + setDiagramList(diagrams); + + if (diagrams.length > 0) { + setDiagramName((prev) => prev || diagrams[0]); + } else { + setDiagramName(null); + } + } + } catch (err) { + console.error(err); + setDiagramList([]); + setDiagramName(null); + } + }; + + load(); + return () => { + isMounted = false; + }; + }, []); + + useEffect(() => { + let isMounted = true; + + const loadDiagram = async () => { + if (!diagramName) { + setNodes([]); + setEdges([]); + return; + } + try { + const data = await fetchDiagramByName(diagramName); + if (isMounted) { + setNodes(data.nodes || []); + const sanitizedEdges = (data.edges || []).map(({ id, source, target, animated }) => ({ + id, + source, + target, + animated: !!animated, + })); + setEdges(sanitizedEdges); + setSelectedNode(null); + setSelectedEdge(null); + } + } catch (err) { + console.error(err); + toast.error(`❌ Failed to load diagram "${diagramName}"`); + setNodes([]); + setEdges([]); + } + }; + + loadDiagram(); + return () => { + isMounted = false; + }; + }, [diagramName, setNodes, setEdges]); const handleSave = async () => { + if (!diagramName) return; const cleanedEdges = edges.map(({ id, source, target, animated }) => ({ id, source, target, animated: !!animated, })); - await saveDiagram({ nodes, edges: cleanedEdges }); - toast.success('✅ Diagram saved!'); + await saveDiagramByName(diagramName, { nodes, edges: cleanedEdges }); + toast.success(`✅ Diagram "${diagramName}" saved!`); }; const handleAddNode = () => { @@ -75,7 +134,6 @@ function Diagram() { const handleDeleteNode = () => { if (!selectedNode) return; - setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id)); setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id)); toast.error(`🗑️ Node "${selectedNode.data.label}" deleted`); @@ -89,7 +147,6 @@ function Diagram() { const handleDeleteEdge = () => { if (!selectedEdge) return; - setEdges((eds) => eds.filter((e) => e.id !== selectedEdge.id)); toast.error(`🗑️ Edge "${selectedEdge.id}" deleted`); setSelectedEdge(null); @@ -116,13 +173,65 @@ function Diagram() { setSelectedNode(node); }; + const handleDeleteDiagram = async () => { + if (!diagramName || diagramName === 'default') { + toast.warn("❌ Cannot delete 'default' diagram or nothing selected."); + return; + } + + const confirmDelete = confirm(`Are you sure you want to delete diagram "${diagramName}"?`); + if (!confirmDelete) return; + + try { + await deleteDiagramByName(diagramName); + const updatedList = diagramList.filter(name => name !== diagramName); + setDiagramList(updatedList); + const newSelected = updatedList[0] || null; + setDiagramName(newSelected); + setNodes([]); + setEdges([]); + toast.success(`🗑️ Diagram "${diagramName}" deleted`); + } catch (err) { + toast.error(`❌ Failed to delete diagram "${diagramName}"`); + } + }; + return (