Add diagrams options
This commit is contained in:
parent
7f199f3789
commit
058614d634
Binary file not shown.
19
backend/diagrams/diagram_argo.json
Normal file
19
backend/diagrams/diagram_argo.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
19
backend/diagrams/diagram_infra.json
Normal file
19
backend/diagrams/diagram_infra.json
Normal file
@ -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": []
|
||||||
|
}
|
||||||
@ -3,10 +3,10 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
from models import DiagramItem
|
from models import DiagramItem
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import requests
|
import requests
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
@ -17,31 +17,54 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Static icon info
|
||||||
BASE_URL = "https://s3.dvirlabs.com/lab-icons"
|
BASE_URL = "https://s3.dvirlabs.com/lab-icons"
|
||||||
S3_INDEX_URL = "https://s3.dvirlabs.com/lab-icons/?list-type=2"
|
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("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
return {"message": "Check if the server is running"}
|
return {"message": "Check if the server is running"}
|
||||||
|
|
||||||
@app.get("/diagram/fetch")
|
@app.get("/diagram/fetch")
|
||||||
def fetch_diagram():
|
def fetch_diagram(name: str):
|
||||||
if not os.path.exists(DATA_FILE):
|
path = DATA_DIR / f"diagram_{name}.json"
|
||||||
|
if not path.exists():
|
||||||
return {"nodes": [], "edges": []}
|
return {"nodes": [], "edges": []}
|
||||||
with open(DATA_FILE, "r") as f:
|
with open(path, "r") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/diagram/save")
|
@app.post("/diagram/save")
|
||||||
def save_diagram(payload: DiagramItem):
|
def save_diagram(name: str, payload: DiagramItem):
|
||||||
try:
|
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)
|
json.dump(payload.dict(), f, indent=2)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(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]])
|
@app.get("/icons", response_model=Dict[str, List[str]])
|
||||||
def list_icons():
|
def list_icons():
|
||||||
@ -79,10 +102,5 @@ def list_icons():
|
|||||||
|
|
||||||
return categories
|
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__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8001)
|
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [],
|
|
||||||
"edges": []
|
|
||||||
}
|
|
||||||
@ -7,7 +7,12 @@ import ReactFlow, {
|
|||||||
useEdgesState,
|
useEdgesState,
|
||||||
} from 'reactflow';
|
} from 'reactflow';
|
||||||
import 'reactflow/dist/style.css';
|
import 'reactflow/dist/style.css';
|
||||||
import { fetchDiagram, saveDiagram } from '../services/api';
|
import {
|
||||||
|
fetchDiagramByName,
|
||||||
|
saveDiagramByName,
|
||||||
|
listDiagrams,
|
||||||
|
deleteDiagramByName,
|
||||||
|
} from '../services/api';
|
||||||
import CustomNode from './CustomNode';
|
import CustomNode from './CustomNode';
|
||||||
import IconSelector from './IconSelector';
|
import IconSelector from './IconSelector';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@ -24,10 +29,49 @@ function Diagram() {
|
|||||||
const [newLabel, setNewLabel] = useState('');
|
const [newLabel, setNewLabel] = useState('');
|
||||||
const [selectedIcon, setSelectedIcon] = useState('');
|
const [selectedIcon, setSelectedIcon] = useState('');
|
||||||
const [selectedEdge, setSelectedEdge] = useState(null);
|
const [selectedEdge, setSelectedEdge] = useState(null);
|
||||||
|
const [diagramName, setDiagramName] = useState(null);
|
||||||
|
const [diagramList, setDiagramList] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDiagram().then((data) => {
|
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 || []);
|
setNodes(data.nodes || []);
|
||||||
const sanitizedEdges = (data.edges || []).map(({ id, source, target, animated }) => ({
|
const sanitizedEdges = (data.edges || []).map(({ id, source, target, animated }) => ({
|
||||||
id,
|
id,
|
||||||
@ -36,18 +80,33 @@ function Diagram() {
|
|||||||
animated: !!animated,
|
animated: !!animated,
|
||||||
}));
|
}));
|
||||||
setEdges(sanitizedEdges);
|
setEdges(sanitizedEdges);
|
||||||
});
|
setSelectedNode(null);
|
||||||
}, [setNodes, setEdges]);
|
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 () => {
|
const handleSave = async () => {
|
||||||
|
if (!diagramName) return;
|
||||||
const cleanedEdges = edges.map(({ id, source, target, animated }) => ({
|
const cleanedEdges = edges.map(({ id, source, target, animated }) => ({
|
||||||
id,
|
id,
|
||||||
source,
|
source,
|
||||||
target,
|
target,
|
||||||
animated: !!animated,
|
animated: !!animated,
|
||||||
}));
|
}));
|
||||||
await saveDiagram({ nodes, edges: cleanedEdges });
|
await saveDiagramByName(diagramName, { nodes, edges: cleanedEdges });
|
||||||
toast.success('✅ Diagram saved!');
|
toast.success(`✅ Diagram "${diagramName}" saved!`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddNode = () => {
|
const handleAddNode = () => {
|
||||||
@ -75,7 +134,6 @@ function Diagram() {
|
|||||||
|
|
||||||
const handleDeleteNode = () => {
|
const handleDeleteNode = () => {
|
||||||
if (!selectedNode) return;
|
if (!selectedNode) return;
|
||||||
|
|
||||||
setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id));
|
setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id));
|
||||||
setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id));
|
setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id));
|
||||||
toast.error(`🗑️ Node "${selectedNode.data.label}" deleted`);
|
toast.error(`🗑️ Node "${selectedNode.data.label}" deleted`);
|
||||||
@ -89,7 +147,6 @@ function Diagram() {
|
|||||||
|
|
||||||
const handleDeleteEdge = () => {
|
const handleDeleteEdge = () => {
|
||||||
if (!selectedEdge) return;
|
if (!selectedEdge) return;
|
||||||
|
|
||||||
setEdges((eds) => eds.filter((e) => e.id !== selectedEdge.id));
|
setEdges((eds) => eds.filter((e) => e.id !== selectedEdge.id));
|
||||||
toast.error(`🗑️ Edge "${selectedEdge.id}" deleted`);
|
toast.error(`🗑️ Edge "${selectedEdge.id}" deleted`);
|
||||||
setSelectedEdge(null);
|
setSelectedEdge(null);
|
||||||
@ -116,13 +173,65 @@ function Diagram() {
|
|||||||
setSelectedNode(node);
|
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 (
|
return (
|
||||||
<div style={{ width: '100vw', height: '100vh' }}>
|
<div style={{ width: '100vw', height: '100vh' }}>
|
||||||
<div style={{ position: 'absolute', zIndex: 10, right: 10, top: 10 }}>
|
<div style={{ position: 'absolute', zIndex: 10, right: 10, top: 10 }}>
|
||||||
<button onClick={handleSave} className="btn">💾 Save</button>
|
<select
|
||||||
|
value={diagramName || ''}
|
||||||
|
onChange={(e) => setDiagramName(e.target.value)}
|
||||||
|
style={{ marginRight: 8, padding: 6 }}
|
||||||
|
>
|
||||||
|
{diagramList.map((name) => (
|
||||||
|
<option key={name} value={name}>{name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={() => {
|
||||||
|
const newName = prompt("Enter new diagram name:");
|
||||||
|
if (newName) {
|
||||||
|
if (!diagramList.includes(newName)) {
|
||||||
|
const updatedList = [...diagramList, newName];
|
||||||
|
setDiagramList(updatedList);
|
||||||
|
}
|
||||||
|
setDiagramName(newName);
|
||||||
|
setNodes([]);
|
||||||
|
setEdges([]);
|
||||||
|
setSelectedNode(null);
|
||||||
|
setSelectedEdge(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
🆕 New Diagram
|
||||||
|
</button>
|
||||||
|
<button onClick={handleSave} className="btn" style={{ marginLeft: 8 }}>💾 Save</button>
|
||||||
<button onClick={handleAddNode} className="btn" style={{ marginLeft: 8 }}>➕ Add Node</button>
|
<button onClick={handleAddNode} className="btn" style={{ marginLeft: 8 }}>➕ Add Node</button>
|
||||||
<button onClick={handleDeleteNode} className="btn" style={{ marginLeft: 8 }} disabled={!selectedNode}>🗑️ Delete Node</button>
|
<button onClick={handleDeleteNode} className="btn" style={{ marginLeft: 8 }} disabled={!selectedNode}>🗑️ Delete Node</button>
|
||||||
<button onClick={handleDeleteEdge} className="btn" style={{ marginLeft: 8 }} disabled={!selectedEdge}>🗑️ Delete Edge</button>
|
<button onClick={handleDeleteEdge} className="btn" style={{ marginLeft: 8 }} disabled={!selectedEdge}>🗑️ Delete Edge</button>
|
||||||
|
<button onClick={handleDeleteDiagram} className="btn" style={{ marginLeft: 8 }}>🗑️ Delete Diagram</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showForm && (
|
{showForm && (
|
||||||
@ -139,19 +248,9 @@ function Diagram() {
|
|||||||
width: 280,
|
width: 280,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h4
|
<h4 style={{ color: '#125ea8', marginBottom: 12, fontSize: 18, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
style={{
|
|
||||||
color: '#125ea8',
|
|
||||||
marginBottom: 12,
|
|
||||||
fontSize: 18,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🧩 Add New Node
|
🧩 Add New Node
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<label style={{ fontWeight: 'bold', marginBottom: 4, display: 'block', color: '#333' }}>
|
<label style={{ fontWeight: 'bold', marginBottom: 4, display: 'block', color: '#333' }}>
|
||||||
Label:
|
Label:
|
||||||
</label>
|
</label>
|
||||||
@ -168,16 +267,10 @@ function Diagram() {
|
|||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconSelector onSelect={setSelectedIcon} />
|
<IconSelector onSelect={setSelectedIcon} />
|
||||||
|
|
||||||
<div style={{ marginTop: 12 }}>
|
<div style={{ marginTop: 12 }}>
|
||||||
<button className="btn" onClick={handleSubmitNode}>✅ Add</button>
|
<button className="btn" onClick={handleSubmitNode}>✅ Add</button>
|
||||||
<button
|
<button className="btn" style={{ marginLeft: 8, background: '#ccc', color: '#333' }} onClick={() => setShowForm(false)}>
|
||||||
className="btn"
|
|
||||||
style={{ marginLeft: 8, background: '#ccc', color: '#333' }}
|
|
||||||
onClick={() => setShowForm(false)}
|
|
||||||
>
|
|
||||||
❌ Cancel
|
❌ Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,27 +1,5 @@
|
|||||||
const API_BASE = window?.ENV?.API_BASE || "";
|
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() {
|
export async function fetchIconCategories() {
|
||||||
const res = await fetch(`${API_BASE}/icons`);
|
const res = await fetch(`${API_BASE}/icons`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@ -30,4 +8,32 @@ export async function fetchIconCategories() {
|
|||||||
return await res.json();
|
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();
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user