Compare commits
31 Commits
toast-aler
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ad6d53261 | |||
| 1d27e4841e | |||
| 95651d6a0a | |||
| ce543dd347 | |||
| 3058fe5285 | |||
| 3b8fb6d55b | |||
| 32b6740669 | |||
| 1b5d7735ce | |||
| 552f351949 | |||
| a599e19ccc | |||
| f48a5210b8 | |||
| 05a8a64039 | |||
| ccc429e258 | |||
| b5d9631b66 | |||
| cd96cbf15d | |||
| 85e86798fc | |||
| 058614d634 | |||
| 7f199f3789 | |||
| 55ff0b24d4 | |||
| 9989174c27 | |||
| 5cb2923859 | |||
| 4c6b651951 | |||
| 7c628f9d31 | |||
| 6a63755cef | |||
| 21b7acae5e | |||
| a63a30e8d8 | |||
| d3b8bf39ad | |||
| 0101a2d40c | |||
| 73dffb2893 | |||
| a8bf733609 | |||
| 337fb30ef0 |
@ -9,12 +9,12 @@ steps:
|
||||
include: [ frontend/** ]
|
||||
settings:
|
||||
registry: harbor.dvirlabs.com
|
||||
repo: my-apps/labmap-frontend
|
||||
repo: my-apps/${CI_REPO_NAME}-frontend
|
||||
dockerfile: frontend/Dockerfile
|
||||
context: frontend
|
||||
tags:
|
||||
- latest
|
||||
- ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}
|
||||
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
|
||||
username:
|
||||
from_secret: DOCKER_USERNAME
|
||||
password:
|
||||
@ -30,12 +30,12 @@ steps:
|
||||
include: [ backend/** ]
|
||||
settings:
|
||||
registry: harbor.dvirlabs.com
|
||||
repo: my-apps/labmap-backend
|
||||
repo: my-apps/${CI_REPO_NAME}-backend
|
||||
dockerfile: backend/Dockerfile
|
||||
context: backend
|
||||
tags:
|
||||
- latest
|
||||
- ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}
|
||||
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
|
||||
username:
|
||||
from_secret: DOCKER_USERNAME
|
||||
password:
|
||||
@ -63,8 +63,8 @@ steps:
|
||||
- |
|
||||
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
||||
echo "💡 Setting frontend tag to: $TAG"
|
||||
yq -i ".frontend.tag = \"$TAG\"" manifests/labmap/values.yaml
|
||||
git add manifests/labmap/values.yaml
|
||||
yq -i ".frontend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
|
||||
git add manifests/${CI_REPO_NAME}/values.yaml
|
||||
git commit -m "frontend: update tag to $TAG" || echo "No changes"
|
||||
git push origin HEAD
|
||||
|
||||
@ -89,7 +89,25 @@ steps:
|
||||
- |
|
||||
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
|
||||
echo "💡 Setting backend tag to: $TAG"
|
||||
yq -i ".backend.tag = \"$TAG\"" manifests/labmap/values.yaml
|
||||
git add manifests/labmap/values.yaml
|
||||
yq -i ".backend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
|
||||
git add manifests/${CI_REPO_NAME}/values.yaml
|
||||
git commit -m "backend: update tag to $TAG" || echo "No changes"
|
||||
git push origin HEAD
|
||||
|
||||
trigger-gitops-via-push:
|
||||
name: Trigger apps-gitops via Git push
|
||||
image: alpine/git
|
||||
environment:
|
||||
GIT_USERNAME:
|
||||
from_secret: GIT_USERNAME
|
||||
GIT_TOKEN:
|
||||
from_secret: GIT_TOKEN
|
||||
commands: |
|
||||
git config --global user.name "woodpecker-bot"
|
||||
git config --global user.email "ci@dvirlabs.com"
|
||||
git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/apps-gitops.git"
|
||||
cd apps-gitops
|
||||
echo "# trigger at $(date) by $${CI_REPO_NAME}" >> .trigger
|
||||
git add .trigger
|
||||
git commit -m "ci: trigger apps-gitops build" || echo "no changes"
|
||||
git push origin HEAD
|
||||
|
||||
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
digrams/
|
||||
Binary file not shown.
4
backend/diagrams/diagram_Exposed services.json
Normal file
4
backend/diagrams/diagram_Exposed services.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"nodes": [],
|
||||
"edges": []
|
||||
}
|
||||
@ -3,56 +3,71 @@ 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()
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # בהמשך תוכל לצמצם לכתובת הפרונטאנד
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
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"
|
||||
|
||||
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):
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="Missing diagram name")
|
||||
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():
|
||||
"""
|
||||
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")
|
||||
@ -79,7 +94,5 @@ def list_icons():
|
||||
|
||||
return categories
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
||||
|
||||
25
diagram.json
25
diagram.json
@ -1,25 +0,0 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "1",
|
||||
"type": "custom",
|
||||
"data": {
|
||||
"label": "Node 1",
|
||||
"icon": "https://s3.dvirlabs.com/lab-icons/dev-tools/argocd.svg"
|
||||
},
|
||||
"position": {
|
||||
"x": 68.5849844537563,
|
||||
"y": 225.18693121594464
|
||||
},
|
||||
"width": 82,
|
||||
"height": 82,
|
||||
"selected": false,
|
||||
"positionAbsolute": {
|
||||
"x": 68.5849844537563,
|
||||
"y": 225.18693121594464
|
||||
},
|
||||
"dragging": false
|
||||
}
|
||||
],
|
||||
"edges": []
|
||||
}
|
||||
@ -2,10 +2,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/labmap.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="/env.js"></script>
|
||||
<title>Vite + React</title>
|
||||
<title>Labmap</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
609
frontend/package-lock.json
generated
609
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,7 +10,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.1.0",
|
||||
"react-confirm-alert": "^3.0.6",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-toastify": "^11.0.5",
|
||||
"reactflow": "^11.11.4"
|
||||
|
||||
64
frontend/public/labmap.svg
Normal file
64
frontend/public/labmap.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 38 KiB |
@ -27,8 +27,8 @@ function CustomNode({ data }) {
|
||||
border: '2px solid #1976d2',
|
||||
borderRadius: 6,
|
||||
textAlign: 'center',
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: 100,
|
||||
height: 100,
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
@ -57,7 +57,7 @@ function CustomNode({ data }) {
|
||||
}}
|
||||
style={{ width: 40, height: 40, marginTop: 6 }}
|
||||
/>
|
||||
<div style={{ fontWeight: 'bold', fontSize: 12 }}>{data.label}</div>
|
||||
<div style={{ fontWeight: 'bold', fontSize: 10 }}>{data.label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -7,49 +7,91 @@ import ReactFlow, {
|
||||
useEdgesState,
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import { fetchDiagram, saveDiagram } from '../services/api';
|
||||
import { confirmAlert } from 'react-confirm-alert';
|
||||
import 'react-confirm-alert/src/react-confirm-alert.css';
|
||||
import '../styles/ConfirmDialog.css';
|
||||
import {
|
||||
fetchDiagramByName,
|
||||
saveDiagramByName,
|
||||
listDiagrams,
|
||||
deleteDiagramByName,
|
||||
} from '../services/api';
|
||||
import CustomNode from './CustomNode';
|
||||
import IconSelector from './IconSelector';
|
||||
import { toast } from 'react-toastify';
|
||||
import RouterNode from './RouterNode';
|
||||
|
||||
const nodeTypes = {
|
||||
custom: CustomNode,
|
||||
router: RouterNode,
|
||||
};
|
||||
|
||||
function Diagram() {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const [selectedNode, setSelectedNode] = useState(null);
|
||||
const [selectedEdge, setSelectedEdge] = useState(null);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [newLabel, setNewLabel] = useState('');
|
||||
const [newType, setNewType] = useState('custom');
|
||||
const [selectedIcon, setSelectedIcon] = useState('');
|
||||
const [diagramName, setDiagramName] = useState(null);
|
||||
const [diagramList, setDiagramList] = useState([]);
|
||||
const isIframe = window.self !== window.top;
|
||||
|
||||
useEffect(() => {
|
||||
fetchDiagram().then((data) => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const { diagrams } = await listDiagrams();
|
||||
setDiagramList(diagrams);
|
||||
if (diagrams.length > 0) {
|
||||
setDiagramName(diagrams[0]);
|
||||
} else {
|
||||
setDiagramName("default"); // <- ensure we use this name
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setDiagramList([]);
|
||||
setDiagramName("default"); // <- fallback to default always
|
||||
}
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const loadDiagram = async () => {
|
||||
if (!diagramName) {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await fetchDiagramByName(diagramName);
|
||||
setNodes(data.nodes || []);
|
||||
const sanitizedEdges = (data.edges || []).map(({ id, source, target, animated }) => ({
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
animated: !!animated,
|
||||
id, source, target, animated: !!animated
|
||||
}));
|
||||
setEdges(sanitizedEdges);
|
||||
});
|
||||
}, [setNodes, setEdges]);
|
||||
setSelectedNode(null);
|
||||
setSelectedEdge(null);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error(`❌ Failed to load diagram "${diagramName}"`);
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
}
|
||||
};
|
||||
loadDiagram();
|
||||
}, [diagramName]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!diagramName) return;
|
||||
const cleanedEdges = edges.map(({ id, source, target, animated }) => ({
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
animated: !!animated,
|
||||
id, source, target, animated: !!animated
|
||||
}));
|
||||
await saveDiagram({ nodes, edges: cleanedEdges });
|
||||
toast.success('✅ Diagram saved!');
|
||||
};
|
||||
|
||||
const handleAddNode = () => {
|
||||
setShowForm(true);
|
||||
await saveDiagramByName(diagramName, { nodes, edges: cleanedEdges });
|
||||
toast.success(`✅ Diagram "${diagramName}" saved!`);
|
||||
};
|
||||
|
||||
const handleSubmitNode = () => {
|
||||
@ -59,8 +101,8 @@ function Diagram() {
|
||||
|
||||
const newNode = {
|
||||
id,
|
||||
type: 'custom',
|
||||
data: { label, icon },
|
||||
type: newType,
|
||||
data: newType === 'custom' ? { label, icon } : { label },
|
||||
position: { x: Math.random() * 400, y: Math.random() * 300 },
|
||||
};
|
||||
|
||||
@ -69,11 +111,11 @@ function Diagram() {
|
||||
setShowForm(false);
|
||||
setNewLabel('');
|
||||
setSelectedIcon('');
|
||||
setNewType('custom');
|
||||
};
|
||||
|
||||
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`);
|
||||
@ -85,7 +127,21 @@ function Diagram() {
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const handleNodeDoubleClick = (event, node) => {
|
||||
const onEdgeClick = (_, edge) => {
|
||||
setSelectedEdge(edge);
|
||||
setSelectedNode(null);
|
||||
};
|
||||
|
||||
const handleDeleteEdge = () => {
|
||||
if (!selectedEdge) return;
|
||||
setEdges((eds) => eds.filter((e) => e.id !== selectedEdge.id));
|
||||
toast.error(`🗑️ Edge "${selectedEdge.id}" deleted`);
|
||||
setSelectedEdge(null);
|
||||
};
|
||||
|
||||
const onNodeClick = (_, node) => setSelectedNode(node);
|
||||
|
||||
const handleNodeDoubleClick = (_, node) => {
|
||||
const newLabel = prompt('Enter new name:', node.data.label);
|
||||
if (newLabel !== null) {
|
||||
setNodes((nds) =>
|
||||
@ -97,73 +153,145 @@ function Diagram() {
|
||||
}
|
||||
};
|
||||
|
||||
const onNodeClick = (_, node) => {
|
||||
setSelectedNode(node);
|
||||
const handleDeleteDiagram = async () => {
|
||||
if (!diagramName || diagramName === 'default') {
|
||||
toast.warn("❌ Cannot delete 'default' diagram or nothing selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
confirmAlert({
|
||||
title: 'Are you sure?',
|
||||
message: `Delete "${diagramName}"?`,
|
||||
buttons: [
|
||||
{
|
||||
label: 'Yes',
|
||||
onClick: async () => {
|
||||
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}"`);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
onClick: () => {} // do nothing
|
||||
}
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ width: '100vw', height: '100vh' }}>
|
||||
<div style={{ position: 'absolute', zIndex: 10, right: 10, top: 10 }}>
|
||||
<button onClick={handleSave} className="btn">💾 Save</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>
|
||||
<div style={{ position: 'absolute', zIndex: 10, right: 10, top: 10, display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<select
|
||||
value={diagramName || ''}
|
||||
onChange={(e) => setDiagramName(e.target.value)}
|
||||
style={{
|
||||
padding: '10px 16px',
|
||||
height: '48px',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '12px',
|
||||
border: 'none',
|
||||
background: '#111',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
appearance: 'none',
|
||||
width: 230,
|
||||
paddingRight: 32,
|
||||
}}
|
||||
>
|
||||
{diagramList.map((name) => (
|
||||
<option key={name} value={name}>
|
||||
📌 {name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div style={{ position: 'absolute', right: 12, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none', color: 'white' }}>
|
||||
⏷
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isIframe && (
|
||||
<>
|
||||
<button className="btn" style={{ background: '#d28519' }} onClick={async () => {
|
||||
const newName = prompt("Enter new diagram name:");
|
||||
if (!newName) return;
|
||||
|
||||
if (diagramList.includes(newName)) {
|
||||
toast.warn("❗ Diagram already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Save current content (only if in 'default')
|
||||
if (diagramName === 'default' && (nodes.length > 0 || edges.length > 0)) {
|
||||
const cleanedEdges = edges.map(({ id, source, target, animated }) => ({
|
||||
id, source, target, animated: !!animated
|
||||
}));
|
||||
await saveDiagramByName(newName, { nodes, edges: cleanedEdges });
|
||||
toast.success(`📁 Migrated content from 'default' to '${newName}'`);
|
||||
}
|
||||
|
||||
setDiagramList((prev) => [...prev, newName]);
|
||||
setDiagramName(newName); // <- only after saving
|
||||
} catch (err) {
|
||||
toast.error(`❌ Failed to migrate default diagram`);
|
||||
console.error(err);
|
||||
}
|
||||
}}>
|
||||
🆕 New Diagram
|
||||
</button>
|
||||
|
||||
|
||||
{diagramName !== 'default' && (
|
||||
<button className="btn" onClick={handleSave} style={{ background: 'green' }}>💾 Save</button>
|
||||
)}
|
||||
<button className="btn" onClick={() => setShowForm(true)} style={{ background: 'blue' }}>➕ Add Node</button>
|
||||
<button className="btn" onClick={handleDeleteNode} style={{ color: 'white', background: selectedNode ? '#b81a1a' : '#424040' }} disabled={!selectedNode}>🗑️ Delete Node</button>
|
||||
<button className="btn" onClick={handleDeleteEdge} style={{ color: 'white', background: selectedEdge ? '#b81a1a' : '#424040' }} disabled={!selectedEdge}>🗑️ Delete Edge</button>
|
||||
<button className="btn" onClick={handleDeleteDiagram} style={{ background: 'red' }}>🗑️ Delete Diagram</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showForm && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
zIndex: 20,
|
||||
right: 10,
|
||||
top: 80,
|
||||
background: '#e6f0fa',
|
||||
padding: '16px',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.15)',
|
||||
width: 280,
|
||||
}}
|
||||
>
|
||||
<h4
|
||||
style={{
|
||||
color: '#125ea8',
|
||||
marginBottom: 12,
|
||||
fontSize: 18,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
🧩 Add New Node
|
||||
</h4>
|
||||
<div style={{ position: 'absolute', zIndex: 20, right: 10, top: 80, background: '#13141a', padding: '16px', borderRadius: '12px', boxShadow: '0 4px 8px rgba(0,0,0,0.15)', width: 280 }}>
|
||||
<h4 style={{ color: 'white', marginBottom: 12 }}>🧩 Add New Node</h4>
|
||||
|
||||
<label style={{ fontWeight: 'bold', marginBottom: 4, display: 'block', color: '#333' }}>
|
||||
Label:
|
||||
</label>
|
||||
<label style={{ fontWeight: 'bold', color: 'white' }}>Label:</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter label"
|
||||
value={newLabel}
|
||||
onChange={(e) => setNewLabel(e.target.value)}
|
||||
style={{
|
||||
padding: '6px',
|
||||
width: '100%',
|
||||
borderRadius: 6,
|
||||
border: '1px solid #ccc',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
style={{ padding: '6px', width: '100%', borderRadius: 6, border: '1px solid #ccc', marginBottom: 12 }}
|
||||
/>
|
||||
|
||||
<IconSelector onSelect={setSelectedIcon} />
|
||||
<label style={{ fontWeight: 'bold', color: 'white' }}>Type:</label>
|
||||
<select
|
||||
value={newType}
|
||||
onChange={(e) => setNewType(e.target.value)}
|
||||
style={{ padding: '6px', width: '100%', borderRadius: 6, border: '1px solid #ccc', marginBottom: 12 }}
|
||||
>
|
||||
<option value="custom">🧱 Custom</option>
|
||||
<option value="router">📡 Router</option>
|
||||
</select>
|
||||
|
||||
{newType === 'custom' && <IconSelector onSelect={setSelectedIcon} />}
|
||||
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<button className="btn" onClick={handleSubmitNode}>✅ Add</button>
|
||||
<button
|
||||
className="btn"
|
||||
style={{ marginLeft: 8, background: '#ccc', color: '#333' }}
|
||||
onClick={() => setShowForm(false)}
|
||||
>
|
||||
❌ Cancel
|
||||
</button>
|
||||
<button className="btn" style={{ marginLeft: 8, background: '#ccc', color: '#333' }} onClick={() => setShowForm(false)}>❌ Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -176,6 +304,11 @@ function Diagram() {
|
||||
onConnect={onConnect}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onPaneClick={() => {
|
||||
setSelectedNode(null);
|
||||
setSelectedEdge(null);
|
||||
}}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
>
|
||||
|
||||
@ -25,7 +25,7 @@ function IconSelector({ onSelect }) {
|
||||
return (
|
||||
<div style={{ marginBottom: '1rem' }}>
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<label style={{ fontWeight: 'bold', color: '#333' }}>Category: </label>
|
||||
<label style={{ fontWeight: 'bold', color: 'white' }}>Category: </label>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={handleCategoryChange}
|
||||
@ -47,7 +47,7 @@ function IconSelector({ onSelect }) {
|
||||
|
||||
{selectedCategory && (
|
||||
<div>
|
||||
<label style={{ fontWeight: 'bold', color: 'black' }}>Choose an icon:</label>
|
||||
<label style={{ fontWeight: 'bold', color: 'white' }}>Choose an icon:</label>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
@ -81,7 +81,7 @@ function IconSelector({ onSelect }) {
|
||||
))}
|
||||
</div>
|
||||
{selectedIcon && (
|
||||
<p style={{ marginTop: 8, fontSize: 13, color: 'black' }}>
|
||||
<p style={{ marginTop: 8, fontSize: 13, color: 'white' }}>
|
||||
Selected: <code>{selectedIcon.split('/').pop()}</code>
|
||||
</p>
|
||||
)}
|
||||
|
||||
14
frontend/src/components/RouterNode.jsx
Normal file
14
frontend/src/components/RouterNode.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
// components/RouterNode.js
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
function RouterNode({ data }) {
|
||||
return (
|
||||
<div style={{ border: '2px dashed #ff9800', borderRadius: 6, padding: 10, width: 100, textAlign: 'center' }}>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<div>📡 {data.label}</div>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RouterNode;
|
||||
@ -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();
|
||||
}
|
||||
|
||||
24
frontend/src/styles/ConfirmDialog.css
Normal file
24
frontend/src/styles/ConfirmDialog.css
Normal file
@ -0,0 +1,24 @@
|
||||
.react-confirm-alert-overlay {
|
||||
background: rgba(0, 0, 0, 0.6); /* כהה ונעים לעין */
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.react-confirm-alert-body {
|
||||
background-color: #e6e3e3;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.react-confirm-alert-button-group button {
|
||||
margin: 0 10px;
|
||||
padding: 8px 18px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
background-color: #333;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-confirm-alert-button-group button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user