Update add node

This commit is contained in:
dvirlabs 2025-06-13 17:24:23 +03:00
parent 1b7347577e
commit 34a65efd2c
5 changed files with 143 additions and 3 deletions

View File

@ -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)

View File

@ -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() {
<button onClick={handleDeleteNode} className="btn" style={{ marginLeft: 8 }} disabled={!selectedNode}>🗑 Delete Node</button>
</div>
{showForm && (
<div style={{ position: 'absolute', zIndex: 20, right: 10, top: 80, background: '#fff', padding: 10, borderRadius: 6, boxShadow: '0 2px 4px rgba(0,0,0,0.2)' }}>
<h4>Add Node</h4>
<input
type="text"
placeholder="Enter label"
value={newLabel}
onChange={(e) => setNewLabel(e.target.value)}
style={{ marginBottom: 8, display: 'block', width: '100%' }}
/>
<IconSelector onSelect={setSelectedIcon} />
<button className="btn" onClick={handleSubmitNode}> Add</button>
<button className="btn" style={{ marginLeft: 8 }} onClick={() => setShowForm(false)}> Cancel</button>
</div>
)}
<ReactFlow
nodes={nodes}
edges={edges}
@ -111,4 +139,4 @@ function Diagram() {
);
}
export default Diagram;
export default Diagram;

View File

@ -0,0 +1,58 @@
import { useEffect, useState } from 'react';
import { fetchIconCategories } from '../services/api';
function IconSelector({ onSelect }) {
const [categories, setCategories] = useState({});
const [selectedCategory, setSelectedCategory] = useState('');
const [selectedIcon, setSelectedIcon] = useState('');
useEffect(() => {
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 (
<div style={{ marginBottom: '1rem' }}>
<label>Category:&nbsp;</label>
<select value={selectedCategory} onChange={handleCategoryChange}>
<option value="">Select Category</option>
{Object.keys(categories).map((cat) => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
{selectedCategory && (
<>
<label style={{ marginLeft: '1rem' }}>Icon:&nbsp;</label>
<select value={selectedIcon} onChange={handleIconChange}>
<option value="">Select Icon</option>
{categories[selectedCategory].map((url) => (
<option key={url} value={url}>{url.split('/').pop()}</option>
))}
</select>
{selectedIcon && (
<img
src={selectedIcon}
alt="preview"
style={{ width: 32, height: 32, marginLeft: 10, verticalAlign: 'middle' }}
/>
)}
</>
)}
</div>
);
}
export default IconSelector;

View File

@ -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();
}