From 058614d6347546c46b9fec325f2f05cd573cd843 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 11 Jul 2025 18:18:08 +0300 Subject: [PATCH] Add diagrams options --- backend/__pycache__/main.cpython-313.pyc | Bin 4068 -> 5514 bytes backend/diagrams/diagram_argo.json | 19 +++ backend/diagrams/diagram_infra.json | 19 +++ backend/main.py | 46 +++++-- diagram.json | 4 - frontend/src/components/Diagram.jsx | 165 ++++++++++++++++++----- frontend/src/services/api.js | 50 ++++--- 7 files changed, 227 insertions(+), 76 deletions(-) create mode 100644 backend/diagrams/diagram_argo.json create mode 100644 backend/diagrams/diagram_infra.json delete mode 100644 diagram.json diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 52df594f5dede109e4a0870cfa38b080897b92ab..19e2d0a3fef0d58f0b2735f47253628166f97f48 100644 GIT binary patch delta 3013 zcma)8Urbxq89(>0uYK)%4K`p*jg13IE-jcXu$6yFU1&x@$tvG4O~R99@CBS0o1JUA z1x+$kngVGmi0-0!TBY(BY0^|Fedxoc&3l}*%?PPMH)M&lzRwYrT4gW$&NVhhYor}? zzjOSZ-}%0CzQ24Ky!*Oz>~c8}jG+rZUH-~>U-HrS==&F^3)7-bhtRYWyFy4sDmNod z%UGUvW4HC2pJ|$I#?2uVr^0B62!Me-aZgOCt{Z8bRc#Tf{t3e@$gtxU)qz{%&0!R8 z8=|WC*ucp-SHt<(CF38QzkvO5zbf{Lpr<3=5gTqmoGewR>KdltPM+B4x9mzaEvoE4 zy>sU)(C#|ADIOf8)k3o$-DK-e4Z*RAZ&BiDJf?c%wn0{HIdd}n3WcrplWh&xI|t}@ ziJrn)WvZ_ZuimfyjFO&LDDlVL@nBqz2RRToNE51d4Abgs4JyyaFQ|c$A0w3XA{6Ih z?^&A9ozWjgYNr}hLtyeQ;?5;ziBZDFKdB!I3T3kICG*QhAN|WVQ%I$gOE|fr-vGX# z{XDJb6S-T)pXnb_9;1!97H9>z0r+$Y$Od}uY$>;Wzbw94X5YkKW0Uc`WwP1iineeP zBGxr{pbUJ+e+BXY%~?!BRB9GUNTn|^Bv7){EEQu&^cY_;O;M(w?nZOu&DNNuRCb82 zu4$B|=ZI8m#Dv=FbPl6g+PKbM8R4$iAC-8?_21NUS%o&aY%ZngCa0yAV4|?XnZjiB zMl><`Zp`GZck4!p^%NM~20~4C6-;DOi#mBmC=|jigzH+=@(as}6Pw{4;B~>L&j8s# zUy7chu;<<5_q1~B8~frL8SI-+6Qx+`*`GomNo*G$`L@^&VdOf8(4 z2ha%F;IoX*f-4@N&R3hAo+CJG%+e~;U}VPl9oI!s#^1OA$Ed8z8UN;57+$p*Hhy@L zovY`)D$L1s8Eie)?ZdQ2sVK`v*+ps*jWa>CNGXo%xE;XQ55!pK$Hh+aD?~__@fW^l znX`rx(odv<%9NU0R6Tn z>SRhtgk{O;i27}%RvTFd9MLOG>4GOZ!jwihSU0#O1BtT$0_vmgjgNu#07X!?!5^`@ z{p)ztt5P*Hde9VH2C@Ui>e+A#oTZGA;4Q@Lr>xpaZSZDISO{H%sx2a@>Fg{Tpt_O5 zU0;_@;wjNYF7>Z+DanXI1IR!ke#Rx`;3O`!`G)?^pc z8BHf)So4vKn4;z*9bbm6OYrFvK%joeO&e23Zr`t6_g&@A{x95-E%s1w@5tM7k^RK} z8~bCQvhPzK_I^5l;2Zuz7Vi_Ie!s=WTc?ko>m~$N&ynmod_zG&FXVtN5}_a$1XHiFGMz!^nuIhhQq=f$zp5F z(lu|W+)0MeQMw^R>vFNL=p zhJ$3ECENU3Hj~cY!o=`+kU*+st(shHU+Sdt{@xR%QHgd$yP1;H6Adz@AP4JpK9XL@Wp(AzA8kDh)o1+6ae?YLL@~>| z;S^Skf;dJ+j6aC;Axy5W*i9B|`YI+3%ycXy^V(7lr!^g4GosE*tyVpESg~pe#y*=^a{BN}? za#1~xN!hg0(&Vddx7=;LpXedY9P}`FEsH$_N~}?Bf5O;GF141?F5~xs2T_XjZJ^*i zih726pQ7(RLDEyy`2>kiko*jV%P9PeLxHErXM7{|jk}REvLPJGS2mn4c;s~N3Ljp6 zEQa>Q&|~r9zIgFK?1gR7vwIU*utLHeX;Uh)8`3@s7(=pBsL&ksT9G+MFdZ|9?ywjY z3DMjg-==RXazObjw2d0u)hh_5y}4sDoj_q#X(rIN&9<%J0oDG3W~ngQghQ~ZILW4e z(_eN54yeuwEl^`c&oP3DfPxjCa4KHIko(SGr4Z}fT0dag4_Wcf%m*{Oj=lCzy`QHJ N*og{*Sifbs{{s4CZh-&* delta 1572 zcmZ8g-A@}w5Z}E!f1K}ZpTCIturc5$TnIR%AwWb(t+o-ECT+M1wdZs?5DeY*?$kHpsw>F-fLA*cGsK}GjUST|vcW}A!bw~a1Rl$nJE0II z;US&~O5t|Y$C6U5Db05btC89qxKuN!dEEjY+WP>#>>Y9sxLdxN_9k6jCLHlN9n z%xurJQ7q8!__wh_IpN|6SlO)pJOO@gmJ ztYGmVd%q+4KyIHNXU+a^B-y6wI5>Z2ZJmYPF_;?e;i4!h*4*ethQ{@A`(6$eP_oJc zwjP(^ya9}8m*xB-e@W#`OQD$64b!4$bMV1LwV7PeFon5HY09+f@8^w@VT!ND--@R% zjK2PwDHP}Qf+;XCOkt*&$*S1I*9>;U5#j}HI;+P`*A7o2;A;UfUIDd)o=MJ?{^kCa z;pGd%U#X9!=tC*G)!4j#^`ToW+tnS>`DuR5_ecz_dv@HumHFHAYo1R&c+Mk7_^}v* z-#>-~g??^`hxv`LBYuJ=tW6^|fk z;M>szof898!S?V3);Nu4ynrVH^B@b>f@Yx}+h7NmbnYiZRcq3HU{7!d*QVL%Whubf zFVW9!=Ol&&X=W-!f3qE3z8+AWo7jC=$+iXnbjFuSkHWqf00iCbguTNN^mV&cDI)8tE=gIt7tJ@>D8M0|ohBh#Kv&t~$4bb7&dvZp%pF9TRWU16$d zlz^#4Qc@%IYww5@VqLIDN&4v@-k9$!D_||n&*cj_!gxXK^qjA8kx`KhGF82Z0cNrO zs$^vqjS^vKlNeL1Bi2SCGpnc5CQq&xh{6J_zkSZzhh~e}>ofW=c?W!qZ-xVOg~u50 zqT^4{scqzXg2LNK+D4+hjoiDay^Pv-QHZwsyA>CbV@vie_xUAxOKw^c>9Sw#`;!xJ z%N~!o<_fbaF2q@G20skmif!Vi3TMSbYh8N?KD9kyRlKx6&>c-+B*?cGHu=UaLAp71 VW9$yUrhXgU6k-(~2~E|;{{!EpORoR` 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 (
- + + + +
{showForm && ( @@ -139,19 +248,9 @@ function Diagram() { width: 280, }} > -

+

🧩 Add New Node

- @@ -168,16 +267,10 @@ function Diagram() { marginBottom: 12, }} /> - -
-
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 5ab7fa0..052f7e8 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,27 +1,5 @@ const API_BASE = window?.ENV?.API_BASE || ""; -export async function fetchDiagram() { - const res = await fetch(`${API_BASE}/diagram/fetch`); - if (!res.ok) { - throw new Error(`Failed to fetch diagram: ${res.status}`); - } - return await res.json(); -} - -export async function saveDiagram(data) { - const res = await fetch(`${API_BASE}/diagram/save`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); - - if (!res.ok) { - throw new Error(`Failed to save diagram: ${res.status}`); - } - - return await res.json(); -} - export async function fetchIconCategories() { const res = await fetch(`${API_BASE}/icons`); if (!res.ok) { @@ -30,4 +8,32 @@ export async function fetchIconCategories() { return await res.json(); } +export async function listDiagrams() { + const res = await fetch(`${API_BASE}/diagram/list`); + if (!res.ok) throw new Error("Failed to list diagrams"); + return await res.json(); +} +export async function fetchDiagramByName(name) { + const res = await fetch(`${API_BASE}/diagram/fetch?name=${name}`); + if (!res.ok) throw new Error(`Failed to load diagram "${name}"`); + return await res.json(); +} + +export async function saveDiagramByName(name, data) { + const res = await fetch(`${API_BASE}/diagram/save?name=${name}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (!res.ok) throw new Error(`Failed to save diagram "${name}"`); + return await res.json(); +} + +export async function deleteDiagramByName(name) { + const res = await fetch(`${API_BASE}/diagram/delete?name=${name}`, { + method: "DELETE", + }); + if (!res.ok) throw new Error(`Failed to delete diagram "${name}"`); + return await res.json(); +}