// Netzwerk-Graph-Ansicht (Nodes, Kanten, Filter) /** * network-view.js * * Darstellung der Netzwerk-Topologie: * - Geräte als Nodes * - Ports als Ankerpunkte * - Verbindungen als Linien * - Freie / selbstdefinierte Verbindungstypen * * Kein Layout-Framework (kein D3, kein Cytoscape) * -> bewusst simpel & erweiterbar */ /* ========================= * Konfiguration * ========================= */ // TODO: Standort / Rack / View-Kontext vom Backend setzen const CONTEXT_ID = null; // TODO: API-Endpunkte definieren const API_LOAD_NETWORK = '/api/network_view.php?action=load'; const API_SAVE_POSITIONS = '/api/network_view.php?action=save_positions'; /* ========================= * State * ========================= */ let svgElement = null; let devices = []; // Geräte inkl. Position let connections = []; // Verbindungen zwischen Ports let selectedDeviceId = null; let isDragging = false; let dragOffset = { x: 0, y: 0 }; /* ========================= * Initialisierung * ========================= */ document.addEventListener('DOMContentLoaded', () => { svgElement = document.querySelector('#network-svg'); if (!svgElement) { console.warn('Network View: #network-svg nicht gefunden'); return; } bindSvgEvents(); loadNetwork(); }); /* ========================= * Events * ========================= */ function bindSvgEvents() { svgElement.addEventListener('mousemove', onMouseMove); svgElement.addEventListener('mouseup', onMouseUp); svgElement.addEventListener('click', onSvgClick); } /* ========================= * Laden * ========================= */ function loadNetwork() { if (!CONTEXT_ID) { console.warn('CONTEXT_ID nicht gesetzt'); return; } fetch(`${API_LOAD_NETWORK}&context_id=${CONTEXT_ID}`) .then(res => res.json()) .then(data => { // TODO: Datenstruktur validieren devices = data.devices || []; connections = data.connections || []; renderAll(); }) .catch(err => { console.error('Fehler beim Laden der Netzwerkansicht', err); }); } /* ========================= * Rendering * ========================= */ function renderAll() { clearSvg(); renderConnections(); renderDevices(); } function clearSvg() { while (svgElement.firstChild) { svgElement.removeChild(svgElement.firstChild); } } /* ---------- Geräte ---------- */ function renderDevices() { devices.forEach(device => renderDevice(device)); } function renderDevice(device) { const group = document.createElementNS('http://www.w3.org/2000/svg', 'g'); group.classList.add('device-node'); group.dataset.id = device.id; group.setAttribute( 'transform', `translate(${device.x || 0}, ${device.y || 0})` ); // TODO: Gerätetyp (SVG oder JPG) korrekt laden const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.setAttribute('width', 120); rect.setAttribute('height', 60); rect.setAttribute('rx', 6); rect.addEventListener('mousedown', (e) => { startDrag(e, device.id); e.stopPropagation(); }); // Label const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); text.setAttribute('x', 60); text.setAttribute('y', 35); text.setAttribute('text-anchor', 'middle'); text.textContent = device.name || 'Device'; group.appendChild(rect); group.appendChild(text); // TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition) // TODO: Ports klickbar machen (für Verbindungs-Erstellung) svgElement.appendChild(group); } /* ---------- Verbindungen ---------- */ function renderConnections() { connections.forEach(conn => renderConnection(conn)); } function renderConnection(connection) { // TODO: Quell- & Ziel-Port-Koordinaten berechnen // TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke) const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); line.setAttribute('x1', 0); line.setAttribute('y1', 0); line.setAttribute('x2', 100); line.setAttribute('y2', 100); line.classList.add('connection-line'); svgElement.appendChild(line); } /* ========================= * Interaktion * ========================= */ function onSvgClick(event) { // Klick ins Leere -> Auswahl aufheben if (event.target === svgElement) { selectedDeviceId = null; updateSelection(); } } function startDrag(event, deviceId) { const device = getDeviceById(deviceId); if (!device) return; isDragging = true; selectedDeviceId = deviceId; const point = getSvgCoordinates(event); dragOffset.x = (device.x || 0) - point.x; dragOffset.y = (device.y || 0) - point.y; updateSelection(); } function onMouseMove(event) { if (!isDragging || !selectedDeviceId) return; const device = getDeviceById(selectedDeviceId); if (!device) return; const point = getSvgCoordinates(event); device.x = point.x + dragOffset.x; device.y = point.y + dragOffset.y; renderAll(); } function onMouseUp() { if (!isDragging) return; isDragging = false; // TODO: Positionen optional automatisch speichern } /* ========================= * Auswahl * ========================= */ function updateSelection() { svgElement.querySelectorAll('.device-node').forEach(el => { el.classList.toggle( 'selected', el.dataset.id === String(selectedDeviceId) ); }); // TODO: Sidebar mit Gerätedetails füllen } /* ========================= * Speichern * ========================= */ function savePositions() { fetch(API_SAVE_POSITIONS, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ context_id: CONTEXT_ID, devices: devices.map(d => ({ id: d.id, x: d.x, y: d.y })) }) }) .then(res => res.json()) .then(data => { // TODO: Erfolg / Fehler anzeigen console.log('Positionen gespeichert', data); }) .catch(err => { console.error('Fehler beim Speichern', err); }); } /* ========================= * Hilfsfunktionen * ========================= */ function getSvgCoordinates(event) { const pt = svgElement.createSVGPoint(); pt.x = event.clientX; pt.y = event.clientY; const transformed = pt.matrixTransform(svgElement.getScreenCTM().inverse()); return { x: transformed.x, y: transformed.y }; } function getDeviceById(id) { return devices.find(d => d.id === id); } /* ========================= * Keyboard Shortcuts * ========================= */ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { selectedDeviceId = null; updateSelection(); } // TODO: Delete -> Gerät entfernen? });