- Added network-view.js for visualizing network topology with devices and connections. - Introduced svg-editor.js for managing ports on device types with drag-and-drop functionality. - Created bootstrap.php for application initialization, including configuration and database connection. - Established config.php for centralized configuration settings. - Developed index.php as the main entry point with module-based routing. - Integrated _sql.php for database abstraction. - Added auth.php for single-user authentication handling. - Included helpers.php for utility functions. - Created modules for managing connections, device types, devices, and floors. - Implemented database schema in init.sql for locations, buildings, floors, rooms, network outlets, devices, and connections. - Added Docker support with docker-compose.yml for web and database services. - Documented database structure and UI/UX concepts in respective markdown files.
290 lines
6.9 KiB
JavaScript
290 lines
6.9 KiB
JavaScript
// 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?
|
|
});
|