Compare commits

..

No commits in common. "master" and "test-branch" have entirely different histories.

25 changed files with 490 additions and 849 deletions

View File

@ -9,17 +9,19 @@ steps:
include: [ frontend/** ]
settings:
registry: harbor.dvirlabs.com
repo: my-apps/${CI_REPO_NAME}-frontend
repo: my-apps/labmap-frontend
dockerfile: frontend/Dockerfile
context: frontend
tags:
- latest
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
- ${CI_COMMIT_SHA:0:7}
- ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
build-backend:
name: Build & Push Backend
image: woodpeckerci/plugin-kaniko
@ -30,12 +32,13 @@ steps:
include: [ backend/** ]
settings:
registry: harbor.dvirlabs.com
repo: my-apps/${CI_REPO_NAME}-backend
repo: my-apps/labmap-backend
dockerfile: backend/Dockerfile
context: backend
tags:
- latest
- ${CI_COMMIT_TAG:-${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}}
- ${CI_COMMIT_SHA:0:7}
- ${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}
username:
from_secret: DOCKER_USERNAME
password:
@ -61,12 +64,16 @@ steps:
- git clone "https://$${GIT_USERNAME}:$${GIT_TOKEN}@git.dvirlabs.com/dvirlabs/my-apps.git"
- cd my-apps
- |
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
if [ "$CI_COMMIT_BRANCH" = "develop" ]; then
TAG="develop-${CI_BUILD_NUMBER}"
else
TAG="${CI_COMMIT_SHA:0:7}"
fi
echo "💡 Setting frontend tag to: $TAG"
yq -i ".frontend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
git add manifests/${CI_REPO_NAME}/values.yaml
yq -i ".frontend.tag = \"$TAG\"" manifests/labmap/values.yaml
git add manifests/labmap/values.yaml
git commit -m "frontend: update tag to $TAG" || echo "No changes"
git push origin HEAD
git push origin master
update-values-backend:
name: Update backend tag in values.yaml
@ -85,29 +92,16 @@ steps:
- apk add --no-cache git yq
- 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/my-apps.git"
- cd my-apps
- |
TAG="${CI_COMMIT_BRANCH}-${CI_COMMIT_SHA:0:7}"
if [ "$CI_COMMIT_BRANCH" = "develop" ]; then
TAG="develop-${CI_BUILD_NUMBER}"
else
TAG="${CI_COMMIT_SHA:0:7}"
fi
echo "💡 Setting backend tag to: $TAG"
yq -i ".backend.tag = \"$TAG\"" manifests/${CI_REPO_NAME}/values.yaml
git add manifests/${CI_REPO_NAME}/values.yaml
yq -i ".backend.tag = \"$TAG\"" manifests/labmap/values.yaml
git add manifests/labmap/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
git push origin master

1
backend/.gitignore vendored
View File

@ -1 +0,0 @@
digrams/

View File

@ -1,4 +0,0 @@
{
"nodes": [],
"edges": []
}

View File

@ -3,71 +3,57 @@ 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_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
BASE_URL = "https://s3.dvirlabs.com/lab-icons"
S3_INDEX_URL = "https://s3.dvirlabs.com/lab-icons/?list-type=2"
BASE_DIR = Path(__file__).parent.resolve()
DATA_DIR = BASE_DIR / "diagrams"
DATA_DIR.mkdir(exist_ok=True)
DATA_FILE = "diagram.json"
@app.get("/")
def root():
return {"message": "Check if the server is running"}
@app.get("/diagram/fetch")
def fetch_diagram(name: str):
path = DATA_DIR / f"diagram_{name}.json"
if not path.exists():
def fetch_diagram():
if not os.path.exists(DATA_FILE):
return {"nodes": [], "edges": []}
with open(path, "r") as f:
with open(DATA_FILE, "r") as f:
return json.load(f)
@app.post("/diagram/save")
def save_diagram(name: str, payload: DiagramItem):
if not name:
raise HTTPException(status_code=400, detail="Missing diagram name")
def save_diagram(payload: DiagramItem):
try:
path = DATA_DIR / f"diagram_{name}.json"
with open(path, "w") as f:
with open(DATA_FILE, "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")
@ -94,5 +80,6 @@ def list_icons():
return categories
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
uvicorn.run(app, host="0.0.0.0", port=8000)

36
diagram.json Normal file
View File

@ -0,0 +1,36 @@
{
"nodes": [
{
"id": "1",
"type": "custom",
"data": {
"label": "ArgoCD",
"icon": "https://s3.dvirlabs.com/lab-icons/argocd.svg"
},
"position": {
"x": 100,
"y": 100
}
},
{
"id": "2",
"type": "custom",
"data": {
"label": "Gitea",
"icon": "https://s3.dvirlabs.com/lab-icons/gitea.svg"
},
"position": {
"x": 300,
"y": 200
}
}
],
"edges": [
{
"id": "e1-2",
"source": "1",
"target": "2",
"animated": true
}
]
}

3
frontend/.gitignore vendored
View File

@ -22,6 +22,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Custom
public/env.js

View File

@ -1,4 +0,0 @@
#!/bin/sh
# Generate env.js from the template
envsubst < /etc/env/env.js.template > /usr/share/nginx/html/env.js

View File

@ -3,6 +3,7 @@ FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
@ -15,15 +16,15 @@ RUN apk add --no-cache dos2unix
# Copy built app
COPY --from=builder /app/dist /usr/share/nginx/html
# ✅ Copy env.js.template
COPY public/env.js.template /etc/env/env.js.template
# ✅ Copy nginx config
# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# ✅ Add env generator script to nginx entrypoint hook
COPY 10-generate-env.sh /docker-entrypoint.d/10-generate-env.sh
RUN dos2unix /docker-entrypoint.d/10-generate-env.sh && chmod +x /docker-entrypoint.d/10-generate-env.sh
# Copy runtime env template + entrypoint
COPY public/env.js.template /usr/share/nginx/html/env.js.template
COPY docker-entrypoint.sh /entrypoint.sh
# Normalize line endings and set permissions
RUN dos2unix /entrypoint.sh && chmod +x /entrypoint.sh
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT ["/entrypoint.sh"]

View File

@ -0,0 +1,9 @@
#!/bin/sh
cat <<EOF > /usr/share/nginx/html/env.js
window.ENV = {
API_BASE: "${API_BASE:-http://localhost:8000}"
};
EOF
exec nginx -g "daemon off;"

View File

@ -2,10 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/labmap.svg" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/env.js"></script>
<title>Labmap</title>
<script src="%PUBLIC_URL%/env.js"></script>
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>

File diff suppressed because it is too large Load Diff

View File

@ -10,11 +10,8 @@
"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"
},
"devDependencies": {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,14 +1,7 @@
import Diagram from './components/Diagram';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
function App() {
return (
<>
<Diagram />
<ToastContainer position="bottom-right" autoClose={3000} />
</>
);
return <Diagram />;
}
export default App;

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="gray" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<path d="M3 9h18M9 21V9" />
</svg>

Before

Width:  |  Height:  |  Size: 271 B

View File

@ -3,61 +3,31 @@ import { Handle, Position } from 'reactflow';
function CustomNode({ data }) {
const icon = data.icon || 'https://s3.dvirlabs.com/lab-icons/default.svg';
const handleSize = 5;
const visibleHandleStyle = {
background: '#1976d2',
width: handleSize,
height: handleSize,
borderRadius: '50%',
zIndex: 2,
};
const invisibleHandleStyle = {
background: 'transparent',
width: handleSize,
height: handleSize,
borderRadius: '50%',
zIndex: 1,
};
return (
<div style={{
background: 'transparent',
border: '2px solid #1976d2',
borderRadius: 6,
padding: 10,
textAlign: 'center',
width: 100,
height: 100,
width: 80,
height: 40,
boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
position: 'relative',
}}>
{/* === TOP === */}
<Handle id="source-top" type="source" position={Position.Top} style={{ ...visibleHandleStyle, top: -6 }} />
<Handle id="target-top" type="target" position={Position.Top} style={{ ...invisibleHandleStyle, top: -6 }} />
{/* Handles - חובה */}
<Handle type="target" position={Position.Top} />
<Handle type="source" position={Position.Bottom} />
{/* === BOTTOM === */}
<Handle id="source-bottom" type="source" position={Position.Bottom} style={{ ...visibleHandleStyle, bottom: -6 }} />
<Handle id="target-bottom" type="target" position={Position.Bottom} style={{ ...invisibleHandleStyle, bottom: -6 }} />
{/* === LEFT === */}
<Handle id="source-left" type="source" position={Position.Left} style={{ ...visibleHandleStyle, left: -6, top: 30 }} />
<Handle id="target-left" type="target" position={Position.Left} style={{ ...invisibleHandleStyle, left: -6, top: 30 }} />
{/* === RIGHT === */}
<Handle id="source-right" type="source" position={Position.Right} style={{ ...visibleHandleStyle, right: -6, top: 30 }} />
<Handle id="target-right" type="target" position={Position.Right} style={{ ...invisibleHandleStyle, right: -6, top: 30 }} />
{/* === ICON + LABEL === */}
<img
src={icon}
alt={data.label}
onError={(e) => {
e.target.src = 'https://s3.dvirlabs.com/lab-icons/default.svg';
}}
style={{ width: 40, height: 40, marginTop: 6 }}
style={{ width: 40, height: 40, marginBottom: 6 }}
/>
<div style={{ fontWeight: 'bold', fontSize: 10 }}>{data.label}</div>
<div style={{ fontWeight: 'bold' }}>{data.label}</div>
</div>
);
}

View File

@ -1,3 +1,4 @@
// src/components/Diagram.jsx
import { useEffect, useState, useCallback } from 'react';
import ReactFlow, {
addEdge,
@ -7,91 +8,48 @@ import ReactFlow, {
useEdgesState,
} from 'reactflow';
import 'reactflow/dist/style.css';
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 { fetchDiagram, saveDiagram } 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(() => {
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);
fetchDiagram().then((data) => {
setNodes(data.nodes || []);
const sanitizedEdges = (data.edges || []).map(({ id, source, target, animated }) => ({
id, source, target, animated: !!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();
}, [diagramName]);
});
}, [setNodes, setEdges]);
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 saveDiagramByName(diagramName, { nodes, edges: cleanedEdges });
toast.success(`✅ Diagram "${diagramName}" saved!`);
await saveDiagram({ nodes, edges: cleanedEdges });
alert('Diagram saved!');
};
const handleAddNode = () => {
setShowForm(true);
};
const handleSubmitNode = () => {
@ -101,24 +59,21 @@ function Diagram() {
const newNode = {
id,
type: newType,
data: newType === 'custom' ? { label, icon } : { label },
type: 'custom',
data: { label, icon },
position: { x: Math.random() * 400, y: Math.random() * 300 },
};
setNodes((nds) => [...nds, newNode]);
toast.success(`🟢 Node "${label}" added`);
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`);
setSelectedNode(null);
};
@ -127,21 +82,7 @@ function Diagram() {
[setEdges]
);
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 handleNodeDoubleClick = (event, node) => {
const newLabel = prompt('Enter new name:', node.data.label);
if (newLabel !== null) {
setNodes((nds) =>
@ -149,149 +90,76 @@ function Diagram() {
n.id === node.id ? { ...n, data: { ...n.data, label: newLabel } } : n
)
);
toast.info(`✏️ Node renamed to "${newLabel}"`);
}
};
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
}
]
});
const onNodeClick = (_, node) => {
setSelectedNode(node);
};
return (
<div style={{ width: '100vw', height: '100vh' }}>
<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 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>
{showForm && (
<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>
<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>
<label style={{ fontWeight: 'bold', color: 'white' }}>Label:</label>
<label style={{ fontWeight: 'bold', marginBottom: 4, display: 'block', color: '#333' }}>
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,
}}
/>
<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} />}
<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>
)}
@ -304,11 +172,6 @@ function Diagram() {
onConnect={onConnect}
onNodeClick={onNodeClick}
onNodeDoubleClick={handleNodeDoubleClick}
onEdgeClick={onEdgeClick}
onPaneClick={() => {
setSelectedNode(null);
setSelectedEdge(null);
}}
nodeTypes={nodeTypes}
fitView
>

View File

@ -25,7 +25,7 @@ function IconSelector({ onSelect }) {
return (
<div style={{ marginBottom: '1rem' }}>
<div style={{ marginBottom: '0.5rem' }}>
<label style={{ fontWeight: 'bold', color: 'white' }}>Category:&nbsp;</label>
<label style={{ fontWeight: 'bold', color: '#333' }}>Category:&nbsp;</label>
<select
value={selectedCategory}
onChange={handleCategoryChange}
@ -47,7 +47,7 @@ function IconSelector({ onSelect }) {
{selectedCategory && (
<div>
<label style={{ fontWeight: 'bold', color: 'white' }}>Choose an icon:</label>
<label style={{ fontWeight: 'bold' }}>Choose an icon:</label>
<div
style={{
display: 'grid',
@ -81,7 +81,7 @@ function IconSelector({ onSelect }) {
))}
</div>
{selectedIcon && (
<p style={{ marginTop: 8, fontSize: 13, color: 'white' }}>
<p style={{ marginTop: 8, fontSize: 13 }}>
Selected: <code>{selectedIcon.split('/').pop()}</code>
</p>
)}

View File

@ -1,14 +0,0 @@
// 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;

View File

@ -1,39 +1,20 @@
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`);
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),
});
return await res.json();
}
export async function fetchIconCategories() {
const res = await fetch(`${API_BASE}/icons`);
if (!res.ok) {
throw new Error(`Failed to fetch icons: ${res.status}`);
}
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();
}

View File

@ -1,24 +0,0 @@
.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;
}

View File

@ -1,7 +1,7 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});
})

View File

@ -19,4 +19,4 @@ spec:
- containerPort: {{ .Values.frontend.port }}
env:
- name: API_BASE
value: {{ .Values.frontend.env.API_BASE | quote }}
value: "https://{{ .Values.backend.ingress.host }}"

View File

@ -5,8 +5,6 @@ frontend:
ingress:
enabled: true
host: labmap.dvirlabs.com
env:
API_BASE: "https://api-labmap.dvirlabs.com"
backend:
image: harbor.dvirlabs.com/my-apps/labmap-backend