div TODOs
This commit is contained in:
@@ -1,61 +1,23 @@
|
||||
// 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
|
||||
* ========================= */
|
||||
const svgElement = document.querySelector('#network-svg');
|
||||
if (!svgElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 CONTEXT_TYPE = svgElement.dataset.contextType || 'all';
|
||||
const CONTEXT_ID = Number(svgElement.dataset.contextId || 0);
|
||||
const API_LOAD_NETWORK = '/api/connections.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 devices = [];
|
||||
let ports = [];
|
||||
let connections = [];
|
||||
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
|
||||
* ========================= */
|
||||
bindSvgEvents();
|
||||
loadNetwork();
|
||||
|
||||
function bindSvgEvents() {
|
||||
svgElement.addEventListener('mousemove', onMouseMove);
|
||||
@@ -63,37 +25,40 @@ function bindSvgEvents() {
|
||||
svgElement.addEventListener('click', onSvgClick);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Laden
|
||||
* ========================= */
|
||||
function buildLoadUrl() {
|
||||
const params = new URLSearchParams();
|
||||
params.set('action', 'load');
|
||||
params.set('context_type', CONTEXT_TYPE);
|
||||
if (CONTEXT_TYPE !== 'all') {
|
||||
params.set('context_id', String(CONTEXT_ID));
|
||||
}
|
||||
return '/api/connections.php?' + params.toString();
|
||||
}
|
||||
|
||||
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 || [];
|
||||
fetch(buildLoadUrl())
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (!data || !Array.isArray(data.devices) || !Array.isArray(data.connections)) {
|
||||
throw new Error('Antwortformat ungueltig');
|
||||
}
|
||||
|
||||
devices = data.devices.map((device, index) => ({
|
||||
...device,
|
||||
x: Number(device.pos_x ?? device.x ?? 50 + (index % 6) * 150),
|
||||
y: Number(device.pos_y ?? device.y ?? 60 + Math.floor(index / 6) * 120)
|
||||
}));
|
||||
ports = Array.isArray(data.ports) ? data.ports : [];
|
||||
connections = data.connections;
|
||||
renderAll();
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
console.error('Fehler beim Laden der Netzwerkansicht', err);
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Rendering
|
||||
* ========================= */
|
||||
|
||||
function renderAll() {
|
||||
clearSvg();
|
||||
|
||||
renderConnections();
|
||||
renderDevices();
|
||||
}
|
||||
@@ -104,34 +69,27 @@ function clearSvg() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Geräte ---------- */
|
||||
|
||||
function renderDevices() {
|
||||
devices.forEach(device => renderDevice(device));
|
||||
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})`);
|
||||
|
||||
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.classList.add('device-node-rect');
|
||||
|
||||
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);
|
||||
@@ -141,40 +99,59 @@ function renderDevice(device) {
|
||||
group.appendChild(rect);
|
||||
group.appendChild(text);
|
||||
|
||||
// TODO: Ports als kleine Kreise anlegen (Position aus Portdefinition)
|
||||
// TODO: Ports klickbar machen (für Verbindungs-Erstellung)
|
||||
const devicePorts = ports.filter((port) => Number(port.device_id) === Number(device.id));
|
||||
const spacing = 120 / (Math.max(1, devicePorts.length) + 1);
|
||||
devicePorts.forEach((port, index) => {
|
||||
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
||||
dot.setAttribute('cx', String(Math.round((index + 1) * spacing)));
|
||||
dot.setAttribute('cy', '62');
|
||||
dot.setAttribute('r', '3');
|
||||
dot.classList.add('device-port-dot');
|
||||
dot.dataset.portId = String(port.id);
|
||||
dot.dataset.deviceId = String(device.id);
|
||||
dot.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
console.info('Port ausgewaehlt', port.id);
|
||||
});
|
||||
group.appendChild(dot);
|
||||
});
|
||||
|
||||
svgElement.appendChild(group);
|
||||
}
|
||||
|
||||
/* ---------- Verbindungen ---------- */
|
||||
|
||||
function renderConnections() {
|
||||
connections.forEach(conn => renderConnection(conn));
|
||||
connections.forEach((connection) => renderConnection(connection));
|
||||
}
|
||||
|
||||
function renderConnection(connection) {
|
||||
// TODO: Quell- & Ziel-Port-Koordinaten berechnen
|
||||
// TODO: unterschiedliche Verbindungstypen (Farbe, Strichart, Dicke)
|
||||
const sourcePort = ports.find((port) => Number(port.id) === Number(connection.port_a_id));
|
||||
const targetPort = ports.find((port) => Number(port.id) === Number(connection.port_b_id));
|
||||
if (!sourcePort || !targetPort) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceDevice = devices.find((device) => Number(device.id) === Number(sourcePort.device_id));
|
||||
const targetDevice = devices.find((device) => Number(device.id) === Number(targetPort.device_id));
|
||||
if (!sourceDevice || !targetDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', String(sourceDevice.x + 60));
|
||||
line.setAttribute('y1', String(sourceDevice.y + 60));
|
||||
line.setAttribute('x2', String(targetDevice.x + 60));
|
||||
line.setAttribute('y2', String(targetDevice.y + 60));
|
||||
|
||||
line.setAttribute('x1', 0);
|
||||
line.setAttribute('y1', 0);
|
||||
line.setAttribute('x2', 100);
|
||||
line.setAttribute('y2', 100);
|
||||
|
||||
const isFiber = String(connection.mode || '').toLowerCase().includes('fiber');
|
||||
line.classList.add('connection-line');
|
||||
line.setAttribute('stroke', isFiber ? '#2f6fef' : '#1f8b4c');
|
||||
line.setAttribute('stroke-width', isFiber ? '2.5' : '2');
|
||||
line.setAttribute('stroke-dasharray', isFiber ? '6 4' : '');
|
||||
|
||||
svgElement.appendChild(line);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Interaktion
|
||||
* ========================= */
|
||||
|
||||
function onSvgClick(event) {
|
||||
// Klick ins Leere -> Auswahl aufheben
|
||||
if (event.target === svgElement) {
|
||||
selectedDeviceId = null;
|
||||
updateSelection();
|
||||
@@ -202,7 +179,6 @@ function onMouseMove(event) {
|
||||
if (!device) return;
|
||||
|
||||
const point = getSvgCoordinates(event);
|
||||
|
||||
device.x = point.x + dragOffset.x;
|
||||
device.y = point.y + dragOffset.y;
|
||||
|
||||
@@ -210,31 +186,28 @@ function onMouseMove(event) {
|
||||
}
|
||||
|
||||
function onMouseUp() {
|
||||
if (!isDragging) return;
|
||||
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)
|
||||
);
|
||||
svgElement.querySelectorAll('.device-node').forEach((el) => {
|
||||
el.classList.toggle('selected', el.dataset.id === String(selectedDeviceId));
|
||||
});
|
||||
|
||||
// TODO: Sidebar mit Gerätedetails füllen
|
||||
}
|
||||
const sidebar = document.querySelector('[data-network-selected-device]');
|
||||
if (!sidebar) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Speichern
|
||||
* ========================= */
|
||||
const device = getDeviceById(selectedDeviceId);
|
||||
sidebar.textContent = device
|
||||
? `${device.name} (ID ${device.id})`
|
||||
: 'Kein Geraet ausgewaehlt';
|
||||
}
|
||||
|
||||
function savePositions() {
|
||||
fetch(API_SAVE_POSITIONS, {
|
||||
@@ -242,27 +215,21 @@ function savePositions() {
|
||||
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
|
||||
}))
|
||||
devices: devices.map((device) => ({ id: device.id, x: device.x, y: device.y }))
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
// TODO: Erfolg / Fehler anzeigen
|
||||
console.log('Positionen gespeichert', data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Fehler beim Speichern', err);
|
||||
});
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data?.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
alert('Positionen gespeichert');
|
||||
})
|
||||
.catch((err) => {
|
||||
alert('Positionen konnten nicht gespeichert werden: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Hilfsfunktionen
|
||||
* ========================= */
|
||||
|
||||
function getSvgCoordinates(event) {
|
||||
const pt = svgElement.createSVGPoint();
|
||||
pt.x = event.clientX;
|
||||
@@ -273,19 +240,23 @@ function getSvgCoordinates(event) {
|
||||
}
|
||||
|
||||
function getDeviceById(id) {
|
||||
return devices.find(d => d.id === id);
|
||||
return devices.find((device) => Number(device.id) === Number(id));
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Keyboard Shortcuts
|
||||
* ========================= */
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
selectedDeviceId = null;
|
||||
updateSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Delete -> Gerät entfernen?
|
||||
if (event.key === 'Delete' && selectedDeviceId) {
|
||||
console.warn('Delete von Geraeten ist in der Netzwerkansicht noch nicht implementiert.');
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() === 's' && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault();
|
||||
savePositions();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user