diff --git a/README.md b/README.md index c58429e..63dc26c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Rückseite und Verkabelung. - Ein oder mehrere Racks gemeinsam anzeigen - Geräte positionsgenau anhand ihrer Höheneinheiten darstellen - Vorder- und Rückseite eines Racks getrennt betrachten +- Auf- und absteigende HE-Nummerierung sowie individuelle Start-HE automatisch berücksichtigen - Rackübergreifende und externe Kabelverbindungen erkennen - Geräte in der Kabeltopologie frei verschieben - Geräte in der Kabeltopologie innerhalb jedes Racks nach ihrer Rackposition sortieren diff --git a/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html b/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html index 96121bf..a6d3928 100644 --- a/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html +++ b/netbox_topology_views/templates/netbox_topology_views/rack_elevation.html @@ -195,6 +195,7 @@ if (!groups.has(groupKey)) groups.set(groupKey, { name: node.rack_name || 'Extern', rackUnits: node.rack_u_height || null, + descUnits: Boolean(node.rack_desc_units), nodes: [], }); groups.get(groupKey).nodes.push(node); @@ -213,7 +214,10 @@ nodeContainer.style.gap = '40px'; nodeContainer.style.rowGap = '40px'; column.appendChild(nodeContainer); - group.nodes.sort((a, b) => (b.position || 0) - (a.position || 0)).forEach(node => { + group.nodes.sort((a, b) => group.descUnits + ? (a.position || 0) - (b.position || 0) + : (b.position || 0) - (a.position || 0) + ).forEach(node => { const item = document.createElement(node.url ? 'a' : 'div'); item.className = 'rack-topology-node'; item.id = `topology-${node.node_id}`; @@ -419,11 +423,16 @@ const devices = Array.from(card.querySelectorAll('.rack-device')).map(device => { const bottom = Number.parseFloat(device.style.bottom) || 0; const height = Number.parseFloat(device.style.height) || (100 / rackUnits); + const deviceHeight = Math.max(height / 100 * rackUnits, 1); + const bottomUnits = bottom / 100 * rackUnits; + const startingUnit = Number(rack.starting_unit) || 1; return { name: device.dataset.deviceName || device.querySelector('span')?.textContent || 'Gerät', type: device.dataset.deviceType || '', - position: bottom / 100 * rackUnits + 1, - height: Math.max(height / 100 * rackUnits, 1), + position: rack.desc_units + ? startingUnit + rackUnits - deviceHeight - bottomUnits + : startingUnit + bottomUnits, + height: deviceHeight, face: device.dataset.face === 'rear' ? 'rear' : 'front', color: device.dataset.deviceColor || '#1685fc', }; @@ -444,18 +453,23 @@ racks.forEach((rack, rackIndex) => { const baseX = 10 + rackIndex * (rackWidth + rackGap); const rackHeight = rack.u_height * unit; + const startingUnit = Number(rack.starting_unit) || 1; vertex(rack.name, `text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;fontSize=16;fontStyle=1;fontColor=${palette.heading};`, baseX, 0, rackWidth, 25); vertex(`${rack.site}${rack.location ? ` · ${rack.location}` : ''} · ${rack.u_height}U`, `text;html=1;strokeColor=none;fillColor=none;align=left;fontSize=10;fontColor=${palette.muted};`, baseX, 24, rackWidth, 20); ['front', 'rear'].forEach((face, faceIndex) => { const x = baseX + labelWidth + faceIndex * (faceWidth + faceGap); vertex(face === 'front' ? 'VORDERSEITE' : 'RÜCKSEITE', `text;html=1;strokeColor=none;fillColor=none;align=center;fontSize=10;fontColor=${palette.muted};`, x, 38, faceWidth, 17); vertex('', `rounded=0;html=1;fillColor=none;strokeColor=${palette.frame};strokeWidth=4;`, x, header, faceWidth, rackHeight); - for (let u = 1; u <= rack.u_height; u++) { - const y = header + rackHeight - u * unit; - if (faceIndex === 0) vertex(String(u), `text;html=1;strokeColor=none;fillColor=none;align=right;fontSize=8;fontColor=${palette.muted};`, x - labelWidth, y, labelWidth - 4, unit); + for (let row = 0; row < rack.u_height; row++) { + const y = header + row * unit; + const unitLabel = rack.desc_units ? startingUnit + row : startingUnit + rack.u_height - 1 - row; + if (faceIndex === 0) vertex(String(unitLabel), `text;html=1;strokeColor=none;fillColor=none;align=right;fontSize=8;fontColor=${palette.muted};`, x - labelWidth, y, labelWidth - 4, unit); } rack.devices.filter(device => device.face === face).forEach(device => { - const y = header + rackHeight - (device.position - 1 + device.height) * unit; + const positionOffset = device.position - startingUnit; + const y = rack.desc_units + ? header + positionOffset * unit + : header + rackHeight - (positionOffset + device.height) * unit; const h = Math.max(device.height * unit, 4); const value = h >= 31 ? `${device.name}
U${device.position} · ${device.type}` : device.name; vertex(value, `rounded=1;html=1;whiteSpace=wrap;overflow=hidden;align=left;verticalAlign=top;spacingLeft=5;spacingTop=2;fillColor=${palette.deviceFill};strokeColor=${device.color};strokeWidth=2;fontColor=${palette.deviceText};fontSize=11;`, x + 3, y + 1, faceWidth - 6, h - 2); @@ -476,19 +490,24 @@ racks.forEach((rack, rackIndex) => { const baseX = 10 + rackIndex * (rackWidth + rackGap); const rackHeight = rack.u_height * unit; + const startingUnit = Number(rack.starting_unit) || 1; content += `${xmlEscape(rack.name)}`; content += `${xmlEscape(rack.site)}${rack.location ? ` · ${xmlEscape(rack.location)}` : ''} · ${rack.u_height}U`; ['front', 'rear'].forEach((face, faceIndex) => { const x = baseX + labelWidth + faceIndex * (faceWidth + faceGap); content += `${face === 'front' ? 'VORDERSEITE' : 'RÜCKSEITE'}`; content += ``; - for (let u = 1; u <= rack.u_height; u++) { - const y = header + rackHeight - u * unit; + for (let row = 0; row < rack.u_height; row++) { + const y = header + row * unit; + const unitLabel = rack.desc_units ? startingUnit + row : startingUnit + rack.u_height - 1 - row; content += ``; - if (faceIndex === 0) content += `${u}`; + if (faceIndex === 0) content += `${unitLabel}`; } rack.devices.filter(device => device.face === face).forEach(device => { - const y = header + rackHeight - (device.position - 1 + device.height) * unit; + const positionOffset = device.position - startingUnit; + const y = rack.desc_units + ? header + positionOffset * unit + : header + rackHeight - (positionOffset + device.height) * unit; const h = Math.max(device.height * unit, 4); content += ``; if (h >= 13) content += `${xmlEscape(device.name.slice(0, 34))}`; diff --git a/netbox_topology_views/views.py b/netbox_topology_views/views.py index 61a89e9..1938208 100644 --- a/netbox_topology_views/views.py +++ b/netbox_topology_views/views.py @@ -928,9 +928,11 @@ def _rack_endpoint(termination): "url": device.get_absolute_url() if device else None, "color": getattr(getattr(device, "role", None), "color", None) or "1685fc", "position": float(device.position or 1) if device else None, - "height": max(getattr(getattr(device, "device_type", None), "u_height", 1) or 1, 1) if device else 1, + "height": float(max(getattr(getattr(device, "device_type", None), "u_height", 1) or 1, 1)) if device else 1, "face": str(device.face or "front").lower() if device else "front", "rack_u_height": getattr(getattr(device, "rack", None), "u_height", None), + "rack_desc_units": getattr(getattr(device, "rack", None), "desc_units", False), + "rack_starting_unit": getattr(getattr(device, "rack", None), "starting_unit", 1), "subtitle": str(device.device_type) if device else getattr(termination, "name", str(termination)), } @@ -965,27 +967,42 @@ class RackElevationView(PermissionRequiredMixin, View): device_ids = [] for device in devices.filter(rack_id__in=rack_ids).order_by("rack_id", "position", "name"): device_ids.append(device.pk) - height = max(getattr(device.device_type, "u_height", 1) or 1, 1) + height = float(max(getattr(device.device_type, "u_height", 1) or 1, 1)) rack_height = max(device.rack.u_height or 1, 1) - device.elevation_bottom = f"{(float(device.position or 1) - 1) / rack_height * 100:.4f}" + rack_start = float(device.rack.starting_unit or 1) + position_offset = float(device.position or rack_start) - rack_start + if device.rack.desc_units: + bottom_units = rack_height - position_offset - height + else: + bottom_units = position_offset + device.elevation_bottom = f"{max(bottom_units, 0) / rack_height * 100:.4f}" device.elevation_height = f"{min(height / rack_height * 100, 100):.4f}" device.elevation_face = str(device.face or "front").lower() rack_devices[device.rack_id].append(device) for rack in rack_list: rack.elevation_devices = rack_devices[rack.pk] - rack.elevation_unit_labels = range(rack.u_height or 0, 0, -1) + rack_start = rack.starting_unit or 1 + if rack.desc_units: + rack.elevation_unit_labels = range(rack_start, rack_start + (rack.u_height or 0)) + else: + rack.elevation_unit_labels = range( + rack_start + (rack.u_height or 0) - 1, + rack_start - 1, + -1, + ) rack_export = [] for rack in rack_list: rack_export.append({ "id": rack.pk, "name": rack.name, "u_height": rack.u_height, + "starting_unit": rack.starting_unit, "desc_units": rack.desc_units, "site": str(rack.site), "location": str(rack.location or ""), "devices": [{ "name": device.name or str(device.device_type), "type": str(device.device_type), "position": float(device.position or 1), - "height": max(device.device_type.u_height or 1, 1), + "height": float(max(device.device_type.u_height or 1, 1)), "face": device.elevation_face, "color": f"#{device.role.color}" if device.role.color else "#1685fc", } for device in rack.elevation_devices],