From 5bb373fe2a3c9a762781c3edb94e2a1f1e1ddcf4 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 12 Sep 2023 13:32:29 -0500 Subject: [PATCH] Improved SVG rendering of multiple rear ports (with positions) per path trace. Include asymmetric path detection --- netbox/dcim/svg/cables.py | 111 +++++++++++++++++-------- netbox/templates/dcim/cable_trace.html | 10 ++- 2 files changed, 84 insertions(+), 37 deletions(-) diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py index 9413726fa..eeafcc50d 100644 --- a/netbox/dcim/svg/cables.py +++ b/netbox/dcim/svg/cables.py @@ -34,9 +34,13 @@ class Node(Hyperlink): radius: Box corner radius, for rounded corners (default: 10) """ - def __init__(self, position, width, url, color, labels, radius=10, **extra): + object = None + + def __init__(self, position, width, url, color, labels, radius=10, object=object, **extra): super(Node, self).__init__(href=url, target='_parent', **extra) + self.object = object + x, y = position # Add the box @@ -77,7 +81,7 @@ class Connector(Group): labels: Iterable of text labels """ - def __init__(self, start, url, color, labels=[], **extra): + def __init__(self, start, url, color, labels=[], description=[], **extra): super().__init__(class_='connector', **extra) self.start = start @@ -104,6 +108,8 @@ class Connector(Group): text_coords = (start[0] + PADDING * 2, cursor - LINE_HEIGHT / 2) text = Text(label, insert=text_coords, class_='bold' if not i else []) link.add(text) + if len(description) > 0: + link.set_desc("\n".join(description)) self.add(link) @@ -206,7 +212,8 @@ class CableTraceSVG: url=f'{self.base_url}{term.get_absolute_url()}', color=self._get_color(term), labels=self._get_labels(term), - radius=5 + radius=5, + object=term ) nodes_height = max(nodes_height, node.box['height']) nodes.append(node) @@ -238,20 +245,39 @@ class CableTraceSVG: Polyline(points=points, style=f'stroke: #{connector.color}'), )) - def draw_cable(self, cable): - labels = [ - f'Cable {cable}', - cable.get_status_display() - ] - if cable.type: - labels.append(cable.get_type_display()) - if cable.length and cable.length_unit: - labels.append(f'{cable.length} {cable.get_length_unit_display()}') + def draw_cable(self, cable, terminations, cable_count=0): + if cable_count > 2: + labels = [f'{cable}'] + description = [ + f'Cable {cable}', + cable.get_status_display() + ] + if cable.type: + description.append(cable.get_type_display()) + if cable.length and cable.length_unit: + description.append(f'{cable.length} {cable.get_length_unit_display()}') + else: + labels = [ + f'Cable {cable}', + cable.get_status_display() + ] + description = [] + if cable.type: + labels.append(cable.get_type_display()) + if cable.length and cable.length_unit: + labels.append(f'{cable.length} {cable.get_length_unit_display()}') + + if len(terminations) == 1: + center = terminations[0].bottom_center[0] + else: + termination_centers = [term.bottom_center[0] for term in terminations] + center = sum(termination_centers) / len(termination_centers) connector = Connector( - start=(self.center + OFFSET, self.cursor), + start=(center, self.cursor), color=cable.color or '000000', url=f'{self.base_url}{cable.get_absolute_url()}', - labels=labels + labels=labels, + description=description ) self.cursor += connector.height @@ -334,34 +360,47 @@ class CableTraceSVG: # Connector (a Cable or WirelessLink) if links: - link = links[0] # Remove Cable from list + link_cables = {} + fanin = False + fanout = False - # Cable - if type(link) is Cable: + # Determine if we have fanins or fanouts + if len(near_ends) > len(set(links)): + self.cursor += FANOUT_HEIGHT + fanin = True + if len(far_ends) > len(set(links)): + fanout = True + cursor = self.cursor + for link in links: + # Cable + if type(link) is Cable and not link_cables.get(link.pk): + self.cursor = cursor + near_end_link_terminations = [term for term in terminations if term.object.cable == link] + cable = self.draw_cable(link, near_end_link_terminations, cable_count=len(links)) + link_cables.update({link.pk: cable}) + self.connectors.append(cable) - # Account for fan-ins height - if len(near_ends) > 1: - self.cursor += FANOUT_HEIGHT + # Draw fan-ins + if len(near_ends) > 1 and fanin: + for term in terminations: + if term.object.cable == link: + self.draw_fanin(term, cable) - cable = self.draw_cable(link) - self.connectors.append(cable) - - # Draw fan-ins - if len(near_ends) > 1: - for term in terminations: - self.draw_fanin(term, cable) - - # WirelessLink - elif type(link) is WirelessLink: - wirelesslink = self.draw_wirelesslink(link) - self.connectors.append(wirelesslink) + # WirelessLink + elif type(link) is WirelessLink: + wirelesslink = self.draw_wirelesslink(link) + self.connectors.append(wirelesslink) # Far end termination(s) if len(far_ends) > 1: - self.cursor += FANOUT_HEIGHT - terminations = self.draw_terminations(far_ends) - for term in terminations: - self.draw_fanout(term, cable) + if fanout: + self.cursor += FANOUT_HEIGHT + terminations = self.draw_terminations(far_ends) + for term in terminations: + if hasattr(term.object, 'cable') and link_cables.get(term.object.cable.pk): + self.draw_fanout(term, link_cables.get(term.object.cable.pk)) + else: + self.draw_terminations(far_ends) elif far_ends: self.draw_terminations(far_ends) else: diff --git a/netbox/templates/dcim/cable_trace.html b/netbox/templates/dcim/cable_trace.html index 12000f09d..5fa56bdba 100644 --- a/netbox/templates/dcim/cable_trace.html +++ b/netbox/templates/dcim/cable_trace.html @@ -23,7 +23,15 @@
- {% if path.is_split %} + {% if path.is_split and path.get_asymmetric_nodes %} +

{% trans "Asymmetric Path" %}!

+

{% trans "The nodes below have no links and result in an asymmetric path" %}:

+ + {% elif path.is_split %}

{% trans "Path split" %}!

{% trans "Select a node below to continue" %}: