diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index d66e1ad..7e71e58 100644 Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/main.py b/backend/main.py index 0143699..fb05c7b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,8 +1,12 @@ from fastapi import FastAPI, HTTPException 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 app = FastAPI() @@ -14,8 +18,11 @@ app.add_middleware( allow_headers=["*"], ) +BASE_URL = "https://s3.dvirlabs.com/lab-icons" +S3_INDEX_URL = "https://s3.dvirlabs.com/lab-icons/?list-type=2" DATA_FILE = "diagram.json" + @app.get("/diagram/fetch") def fetch_diagram(): if not os.path.exists(DATA_FILE): @@ -23,6 +30,7 @@ def fetch_diagram(): with open(DATA_FILE, "r") as f: return json.load(f) + @app.post("/diagram/save") def save_diagram(payload: DiagramItem): try: @@ -31,3 +39,44 @@ def save_diagram(payload: DiagramItem): return {"status": "ok"} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/icons", response_model=Dict[str, List[str]]) +def list_icons(): + """ + Returns a dictionary of available icons grouped by folder (category). + Example: + { + "dev-tools": [ "https://s3.dvirlabs.com/lab-icons/dev-tools/gitea.svg", ... ], + "observability": [ ... ] + } + """ + resp = requests.get(S3_INDEX_URL) + if resp.status_code != 200: + raise HTTPException(status_code=500, detail="Failed to fetch icon list from S3") + + root = ET.fromstring(resp.content) + categories: Dict[str, List[str]] = {} + + for content in root.findall(".//{http://s3.amazonaws.com/doc/2006-03-01/}Contents"): + key = content.find("{http://s3.amazonaws.com/doc/2006-03-01/}Key").text + if not key.endswith(".svg"): + continue + parts = key.split('/') + if len(parts) == 2: + category, icon = parts + elif len(parts) > 2: + category = parts[0] + icon = parts[-1] + else: + category = "uncategorized" + icon = parts[0] + + url = f"{BASE_URL}/{key}" + categories.setdefault(category, []).append(url) + + return categories + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/frontend/src/components/Diagram.jsx b/frontend/src/components/Diagram.jsx index bc67b8d..030608e 100644 --- a/frontend/src/components/Diagram.jsx +++ b/frontend/src/components/Diagram.jsx @@ -1,3 +1,4 @@ +// src/components/Diagram.jsx import { useEffect, useState, useCallback } from 'react'; import ReactFlow, { addEdge, @@ -9,6 +10,7 @@ import ReactFlow, { import 'reactflow/dist/style.css'; import { fetchDiagram, saveDiagram } from '../services/api'; import CustomNode from './CustomNode'; +import IconSelector from './IconSelector'; const nodeTypes = { custom: CustomNode, @@ -18,6 +20,9 @@ function Diagram() { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [selectedNode, setSelectedNode] = useState(null); + const [showForm, setShowForm] = useState(false); + const [newLabel, setNewLabel] = useState(''); + const [selectedIcon, setSelectedIcon] = useState(''); useEffect(() => { fetchDiagram().then((data) => { @@ -44,9 +49,13 @@ function Diagram() { }; const handleAddNode = () => { + setShowForm(true); + }; + + const handleSubmitNode = () => { const id = (nodes.length + 1).toString(); - const icon = prompt('Enter icon URL:', 'https://s3.dvirlabs.com/lab-icons/default.svg') || 'https://s3.dvirlabs.com/lab-icons/default.svg'; - const label = prompt('Enter label:', `Node ${id}`) || `Node ${id}`; + const label = newLabel || `Node ${id}`; + const icon = selectedIcon || 'https://s3.dvirlabs.com/lab-icons/default.svg'; const newNode = { id, @@ -56,6 +65,9 @@ function Diagram() { }; setNodes((nds) => [...nds, newNode]); + setShowForm(false); + setNewLabel(''); + setSelectedIcon(''); }; const handleDeleteNode = () => { @@ -93,6 +105,22 @@ function Diagram() { + {showForm && ( +
+

Add Node

+ setNewLabel(e.target.value)} + style={{ marginBottom: 8, display: 'block', width: '100%' }} + /> + + + +
+ )} + { + fetchIconCategories().then(setCategories); + }, []); + + const handleCategoryChange = (e) => { + const category = e.target.value; + setSelectedCategory(category); + setSelectedIcon(''); + onSelect(''); + }; + + const handleIconChange = (e) => { + const icon = e.target.value; + setSelectedIcon(icon); + onSelect(icon); + }; + + return ( +
+ + + + {selectedCategory && ( + <> + + + {selectedIcon && ( + preview + )} + + )} +
+ ); +} + +export default IconSelector; diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 252e06e..55f84e7 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -13,3 +13,8 @@ export async function saveDiagram(data) { }); return await res.json(); } + +export async function fetchIconCategories() { + const res = await fetch(`${API_BASE}/icons`); + return await res.json(); +}