Compare commits
20 Commits
da7c14d4e0
...
91a97fb7c6
Author | SHA1 | Date |
---|---|---|
Arthur Hanson | 91a97fb7c6 | |
Daniel Sheppard | 85db007ff5 | |
Daniel Sheppard | cad3e34d8f | |
Daniel Sheppard | 7b1b91b8ee | |
Daniel Sheppard | 6f36b8513c | |
Daniel Sheppard | 07e2cf0ad2 | |
Arthur | 397d475159 | |
Per von Zweigbergk | 8fadd6b744 | |
Per von Zweigbergk | c93413dc9c | |
Per von Zweigbergk | bf362f4679 | |
Per von Zweigbergk | da7f67c359 | |
Moritz Geist | 2c93dd03e1 | |
Moritz Geist | ced44832f7 | |
Moritz Geist | 6af3aad362 | |
Per von Zweigbergk | c728d3c2e8 | |
Per von Zweigbergk | 83e2c45e74 | |
Per von Zweigbergk | 27864ec865 | |
Per von Zweigbergk | d44f67aea5 | |
Per von Zweigbergk | 41e1f24cf7 | |
Per von Zweigbergk | d76ede17d3 |
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
## v3.7.7 (FUTURE)
|
## v3.7.7 (FUTURE)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#13712](https://github.com/netbox-community/netbox/issues/13712) - Fix row highlighting for device interface list display
|
||||||
|
* [#13806](https://github.com/netbox-community/netbox/issues/13806) - Fix "mark" button tooltip on button activation for device interface list display
|
||||||
|
* [#13922](https://github.com/netbox-community/netbox/issues/13922) - Fix SVG drawing error on multiple termination trace with multiple devices
|
||||||
|
* [#14241](https://github.com/netbox-community/netbox/issues/14241) - Fix random interface swap when performing cable trace with multiple termination
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v3.7.6 (2024-04-22)
|
## v3.7.6 (2024-04-22)
|
||||||
|
|
|
@ -47,6 +47,13 @@ class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SerializedPKRelatedFieldFix(OpenApiSerializerFieldExtension):
|
||||||
|
target_class = "netbox.api.fields.SerializedPKRelatedField"
|
||||||
|
|
||||||
|
def map_serializer_field(self, auto_schema, direction):
|
||||||
|
return auto_schema._map_serializer(self.target.serializer, direction)
|
||||||
|
|
||||||
|
|
||||||
class NetBoxAutoSchema(AutoSchema):
|
class NetBoxAutoSchema(AutoSchema):
|
||||||
"""
|
"""
|
||||||
Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
|
Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
|
||||||
|
|
|
@ -8,17 +8,16 @@ from django.conf import settings
|
||||||
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||||
from utilities.utils import foreground_color
|
from utilities.utils import foreground_color
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableTraceSVG',
|
'CableTraceSVG',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
OFFSET = 0.5
|
OFFSET = 0.5
|
||||||
PADDING = 10
|
PADDING = 10
|
||||||
LINE_HEIGHT = 20
|
LINE_HEIGHT = 20
|
||||||
FANOUT_HEIGHT = 35
|
FANOUT_HEIGHT = 35
|
||||||
FANOUT_LEG_HEIGHT = 15
|
FANOUT_LEG_HEIGHT = 15
|
||||||
|
CABLE_HEIGHT = 4 * LINE_HEIGHT + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
|
||||||
|
|
||||||
|
|
||||||
class Node(Hyperlink):
|
class Node(Hyperlink):
|
||||||
|
@ -84,31 +83,38 @@ class Connector(Group):
|
||||||
labels: Iterable of text labels
|
labels: Iterable of text labels
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, start, url, color, labels=[], description=[], **extra):
|
def __init__(self, start, url, color, wireless, labels=[], description=[], end=None, text_offset=0, **extra):
|
||||||
super().__init__(class_='connector', **extra)
|
super().__init__(class_="connector", **extra)
|
||||||
|
|
||||||
self.start = start
|
self.start = start
|
||||||
self.height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
self.height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
||||||
self.end = (start[0], start[1] + self.height)
|
# Allow to specify end-position or auto-calculate
|
||||||
|
self.end = end if end else (start[0], start[1] + self.height)
|
||||||
self.color = color or '000000'
|
self.color = color or '000000'
|
||||||
|
|
||||||
# Draw a "shadow" line to give the cable a border
|
if wireless:
|
||||||
cable_shadow = Line(start=self.start, end=self.end, class_='cable-shadow')
|
# Draw the cable
|
||||||
self.add(cable_shadow)
|
cable = Line(start=self.start, end=self.end, class_="wireless-link")
|
||||||
|
self.add(cable)
|
||||||
|
else:
|
||||||
|
# Draw a "shadow" line to give the cable a border
|
||||||
|
cable_shadow = Line(start=self.start, end=self.end, class_='cable-shadow')
|
||||||
|
self.add(cable_shadow)
|
||||||
|
|
||||||
# Draw the cable
|
# Draw the cable
|
||||||
cable = Line(start=self.start, end=self.end, style=f'stroke: #{self.color}')
|
cable = Line(start=self.start, end=self.end, style=f'stroke: #{self.color}')
|
||||||
self.add(cable)
|
self.add(cable)
|
||||||
|
|
||||||
# Add link
|
# Add link
|
||||||
link = Hyperlink(href=url, target='_parent')
|
link = Hyperlink(href=url, target='_parent')
|
||||||
|
|
||||||
# Add text label(s)
|
# Add text label(s)
|
||||||
cursor = start[1]
|
cursor = start[1] + text_offset
|
||||||
cursor += PADDING * 2
|
cursor += PADDING * 2 + LINE_HEIGHT * 2
|
||||||
|
x_coord = (start[0] + end[0]) / 2 + PADDING
|
||||||
for i, label in enumerate(labels):
|
for i, label in enumerate(labels):
|
||||||
cursor += LINE_HEIGHT
|
cursor += LINE_HEIGHT
|
||||||
text_coords = (start[0] + PADDING * 2, cursor - LINE_HEIGHT / 2)
|
text_coords = (x_coord, cursor - LINE_HEIGHT / 2)
|
||||||
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
||||||
link.add(text)
|
link.add(text)
|
||||||
if len(description) > 0:
|
if len(description) > 0:
|
||||||
|
@ -190,8 +196,9 @@ class CableTraceSVG:
|
||||||
|
|
||||||
def draw_parent_objects(self, obj_list):
|
def draw_parent_objects(self, obj_list):
|
||||||
"""
|
"""
|
||||||
Draw a set of parent objects.
|
Draw a set of parent objects (eg hosts, switched, patchpanels) and return all created nodes
|
||||||
"""
|
"""
|
||||||
|
objects = []
|
||||||
width = self.width / len(obj_list)
|
width = self.width / len(obj_list)
|
||||||
for i, obj in enumerate(obj_list):
|
for i, obj in enumerate(obj_list):
|
||||||
node = Node(
|
node = Node(
|
||||||
|
@ -199,23 +206,26 @@ class CableTraceSVG:
|
||||||
width=width,
|
width=width,
|
||||||
url=f'{self.base_url}{obj.get_absolute_url()}',
|
url=f'{self.base_url}{obj.get_absolute_url()}',
|
||||||
color=self._get_color(obj),
|
color=self._get_color(obj),
|
||||||
labels=self._get_labels(obj)
|
labels=self._get_labels(obj),
|
||||||
|
object=obj
|
||||||
)
|
)
|
||||||
|
objects.append(node)
|
||||||
self.parent_objects.append(node)
|
self.parent_objects.append(node)
|
||||||
if i + 1 == len(obj_list):
|
if i + 1 == len(obj_list):
|
||||||
self.cursor += node.box['height']
|
self.cursor += node.box['height']
|
||||||
|
return objects
|
||||||
|
|
||||||
def draw_terminations(self, terminations):
|
def draw_object_terminations(self, terminations, offset_x, width):
|
||||||
"""
|
"""
|
||||||
Draw a row of terminating objects (e.g. interfaces), all of which are attached to the same end of a cable.
|
Draw all terminations belonging to an object with specified offset and width
|
||||||
|
Return all created nodes and their maximum height
|
||||||
"""
|
"""
|
||||||
nodes = []
|
|
||||||
nodes_height = 0
|
nodes_height = 0
|
||||||
width = self.width / len(terminations)
|
nodes = []
|
||||||
|
# Sort them by name to make renders more readable
|
||||||
for i, term in enumerate(terminations):
|
for i, term in enumerate(sorted(terminations, key=lambda x: x.name)):
|
||||||
node = Node(
|
node = Node(
|
||||||
position=(i * width, self.cursor),
|
position=(offset_x + i * width, self.cursor),
|
||||||
width=width,
|
width=width,
|
||||||
url=f'{self.base_url}{term.get_absolute_url()}',
|
url=f'{self.base_url}{term.get_absolute_url()}',
|
||||||
color=self._get_color(term),
|
color=self._get_color(term),
|
||||||
|
@ -225,133 +235,89 @@ class CableTraceSVG:
|
||||||
)
|
)
|
||||||
nodes_height = max(nodes_height, node.box['height'])
|
nodes_height = max(nodes_height, node.box['height'])
|
||||||
nodes.append(node)
|
nodes.append(node)
|
||||||
|
return nodes, nodes_height
|
||||||
|
|
||||||
|
def draw_terminations(self, terminations, parent_object_nodes):
|
||||||
|
"""
|
||||||
|
Draw a row of terminating objects (e.g. interfaces) and return all created nodes
|
||||||
|
Attach them to previously created parent objects
|
||||||
|
"""
|
||||||
|
nodes = []
|
||||||
|
nodes_height = 0
|
||||||
|
|
||||||
|
# Draw terminations for each parent object
|
||||||
|
for parent in parent_object_nodes:
|
||||||
|
parent_terms = [term for term in terminations if term.parent_object == parent.object]
|
||||||
|
|
||||||
|
# Width and offset(position) for each termination box
|
||||||
|
width = parent.box['width'] / len(parent_terms)
|
||||||
|
offset_x = parent.box['x']
|
||||||
|
|
||||||
|
result, nodes_height = self.draw_object_terminations(parent_terms, offset_x, width)
|
||||||
|
nodes.extend(result)
|
||||||
|
|
||||||
self.cursor += nodes_height
|
self.cursor += nodes_height
|
||||||
self.terminations.extend(nodes)
|
self.terminations.extend(nodes)
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
def draw_fanin(self, node, connector):
|
def draw_far_objects(self, obj_list, terminations):
|
||||||
points = (
|
|
||||||
node.bottom_center,
|
|
||||||
(node.bottom_center[0], node.bottom_center[1] + FANOUT_LEG_HEIGHT),
|
|
||||||
connector.start,
|
|
||||||
)
|
|
||||||
self.connectors.extend((
|
|
||||||
Polyline(points=points, class_='cable-shadow'),
|
|
||||||
Polyline(points=points, style=f'stroke: #{connector.color}'),
|
|
||||||
))
|
|
||||||
|
|
||||||
def draw_fanout(self, node, connector):
|
|
||||||
points = (
|
|
||||||
connector.end,
|
|
||||||
(node.top_center[0], node.top_center[1] - FANOUT_LEG_HEIGHT),
|
|
||||||
node.top_center,
|
|
||||||
)
|
|
||||||
self.connectors.extend((
|
|
||||||
Polyline(points=points, class_='cable-shadow'),
|
|
||||||
Polyline(points=points, style=f'stroke: #{connector.color}'),
|
|
||||||
))
|
|
||||||
|
|
||||||
def draw_cable(self, cable, terminations, cable_count=0):
|
|
||||||
"""
|
"""
|
||||||
Draw a single cable. Terminations and cable count are passed for determining position and padding
|
Draw the far-end objects and its terminations and return all created nodes
|
||||||
|
|
||||||
:param cable: The cable to draw
|
|
||||||
:param terminations: List of terminations to build positioning data off of
|
|
||||||
:param cable_count: Count of all cables on this layer for determining whether to collapse description into a
|
|
||||||
tooltip.
|
|
||||||
"""
|
"""
|
||||||
|
# Make sure elements are sorted by name for readability
|
||||||
|
objects = sorted(obj_list, key=lambda x: x.name)
|
||||||
|
width = self.width / len(objects)
|
||||||
|
|
||||||
# If the cable count is higher than 2, collapse the description into a tooltip
|
# Max-height of created terminations
|
||||||
if cable_count > 2:
|
terms_height = 0
|
||||||
# Use the cable __str__ function to denote the cable
|
term_nodes = []
|
||||||
labels = [f'{cable}']
|
|
||||||
|
|
||||||
# Include the label and the status description in the tooltip
|
# Draw the terminations by per object first
|
||||||
description = [
|
for i, obj in enumerate(objects):
|
||||||
f'Cable {cable}',
|
obj_terms = [term for term in terminations if term.parent_object == obj]
|
||||||
cable.get_status_display()
|
obj_pos = i * width
|
||||||
]
|
result, result_nodes_height = self.draw_object_terminations(obj_terms, obj_pos, width / len(obj_terms))
|
||||||
|
|
||||||
if cable.type:
|
terms_height = max(terms_height, result_nodes_height)
|
||||||
# Include the cable type in the tooltip
|
term_nodes.extend(result)
|
||||||
description.append(cable.get_type_display())
|
|
||||||
if cable.length is not None and cable.length_unit:
|
|
||||||
# Include the cable length in the tooltip
|
|
||||||
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 is not None and cable.length_unit:
|
|
||||||
# Include the cable length in the tooltip
|
|
||||||
labels.append(f'{cable.length} {cable.get_length_unit_display()}')
|
|
||||||
|
|
||||||
# If there is only one termination, center on that termination
|
# Update cursor and draw the objects
|
||||||
# Otherwise average the center across the terminations
|
self.cursor += terms_height
|
||||||
if len(terminations) == 1:
|
self.terminations.extend(term_nodes)
|
||||||
center = terminations[0].bottom_center[0]
|
object_nodes = self.draw_parent_objects(objects)
|
||||||
else:
|
|
||||||
# Get a list of termination centers
|
|
||||||
termination_centers = [term.bottom_center[0] for term in terminations]
|
|
||||||
# Average the centers
|
|
||||||
center = sum(termination_centers) / len(termination_centers)
|
|
||||||
|
|
||||||
# Create the connector
|
return object_nodes, term_nodes
|
||||||
connector = Connector(
|
|
||||||
start=(center, self.cursor),
|
|
||||||
color=cable.color or '000000',
|
|
||||||
url=f'{self.base_url}{cable.get_absolute_url()}',
|
|
||||||
labels=labels,
|
|
||||||
description=description
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set the cursor position
|
def draw_fanin(self, target, terminations, color):
|
||||||
self.cursor += connector.height
|
|
||||||
|
|
||||||
return connector
|
|
||||||
|
|
||||||
def draw_wirelesslink(self, wirelesslink):
|
|
||||||
"""
|
"""
|
||||||
Draw a line with labels representing a WirelessLink.
|
Draw the fan-in-lines from each of the terminations to the targetpoint
|
||||||
"""
|
"""
|
||||||
group = Group(class_='connector')
|
for term in terminations:
|
||||||
|
points = (
|
||||||
|
term.bottom_center,
|
||||||
|
(term.bottom_center[0], term.bottom_center[1] + FANOUT_LEG_HEIGHT),
|
||||||
|
target,
|
||||||
|
)
|
||||||
|
self.connectors.extend((
|
||||||
|
Polyline(points=points, class_='cable-shadow'),
|
||||||
|
Polyline(points=points, style=f'stroke: #{color}'),
|
||||||
|
))
|
||||||
|
|
||||||
labels = [
|
def draw_fanout(self, start, terminations, color):
|
||||||
f'Wireless link {wirelesslink}',
|
"""
|
||||||
wirelesslink.get_status_display()
|
Draw the fan-out-lines from the startpoint to each of the terminations
|
||||||
]
|
"""
|
||||||
if wirelesslink.ssid:
|
for term in terminations:
|
||||||
labels.append(wirelesslink.ssid)
|
points = (
|
||||||
|
term.top_center,
|
||||||
# Draw the wireless link
|
(term.top_center[0], term.top_center[1] - FANOUT_LEG_HEIGHT),
|
||||||
start = (OFFSET + self.center, self.cursor)
|
start,
|
||||||
height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
)
|
||||||
end = (start[0], start[1] + height)
|
self.connectors.extend((
|
||||||
line = Line(start=start, end=end, class_='wireless-link')
|
Polyline(points=points, class_='cable-shadow'),
|
||||||
group.add(line)
|
Polyline(points=points, style=f'stroke: #{color}'),
|
||||||
|
))
|
||||||
self.cursor += PADDING * 2
|
|
||||||
|
|
||||||
# Add link
|
|
||||||
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_parent')
|
|
||||||
|
|
||||||
# Add text label(s)
|
|
||||||
for i, label in enumerate(labels):
|
|
||||||
self.cursor += LINE_HEIGHT
|
|
||||||
text_coords = (self.center + PADDING * 2, self.cursor - LINE_HEIGHT / 2)
|
|
||||||
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
|
||||||
link.add(text)
|
|
||||||
|
|
||||||
group.add(link)
|
|
||||||
self.cursor += PADDING * 2
|
|
||||||
|
|
||||||
return group
|
|
||||||
|
|
||||||
def draw_attachment(self):
|
def draw_attachment(self):
|
||||||
"""
|
"""
|
||||||
|
@ -378,86 +344,99 @@ class CableTraceSVG:
|
||||||
|
|
||||||
traced_path = self.origin.trace()
|
traced_path = self.origin.trace()
|
||||||
|
|
||||||
|
parent_object_nodes = []
|
||||||
# Iterate through each (terms, cable, terms) segment in the path
|
# Iterate through each (terms, cable, terms) segment in the path
|
||||||
for i, segment in enumerate(traced_path):
|
for i, segment in enumerate(traced_path):
|
||||||
near_ends, links, far_ends = segment
|
near_ends, links, far_ends = segment
|
||||||
|
|
||||||
# Near end parent
|
# This is segment number one.
|
||||||
if i == 0:
|
if i == 0:
|
||||||
# If this is the first segment, draw the originating termination's parent object
|
# If this is the first segment, draw the originating termination's parent object
|
||||||
self.draw_parent_objects(set(end.parent_object for end in near_ends))
|
parent_object_nodes = self.draw_parent_objects(set(end.parent_object for end in near_ends))
|
||||||
|
# Else: No need to draw parent objects (parent objects are drawn in last "round" as the far-end!)
|
||||||
|
|
||||||
# Near end termination(s)
|
near_terminations = self.draw_terminations(near_ends, parent_object_nodes)
|
||||||
terminations = self.draw_terminations(near_ends)
|
self.cursor += CABLE_HEIGHT
|
||||||
|
|
||||||
# Connector (a Cable or WirelessLink)
|
# Connector (a Cable or WirelessLink)
|
||||||
if links:
|
if links:
|
||||||
link_cables = {}
|
|
||||||
fanin = False
|
|
||||||
fanout = False
|
|
||||||
|
|
||||||
# Determine if we have fanins or fanouts
|
parent_object_nodes, far_terminations = self.draw_far_objects(set(end.parent_object for end in far_ends), far_ends)
|
||||||
if len(near_ends) > len(set(links)):
|
for cable in links:
|
||||||
self.cursor += FANOUT_HEIGHT
|
# Fill in labels and description with all available data
|
||||||
fanin = True
|
description = [
|
||||||
if len(far_ends) > len(set(links)):
|
f"Link {cable}",
|
||||||
fanout = True
|
cable.get_status_display()
|
||||||
cursor = self.cursor
|
]
|
||||||
for link in links:
|
near = []
|
||||||
# Cable
|
far = []
|
||||||
if type(link) is Cable and not link_cables.get(link.pk):
|
color = '000000'
|
||||||
# Reset cursor
|
if cable.description:
|
||||||
self.cursor = cursor
|
description.append(f"{cable.description}")
|
||||||
# Generate a list of terminations connected to this cable
|
if isinstance(cable, Cable):
|
||||||
near_end_link_terminations = [term for term in terminations if term.object.cable == link]
|
labels = [f"{cable}"] if len(links) > 2 else [f"Cable {cable}", cable.get_status_display()]
|
||||||
# Draw the cable
|
if cable.type:
|
||||||
cable = self.draw_cable(link, near_end_link_terminations, cable_count=len(links))
|
description.append(cable.get_type_display())
|
||||||
# Add cable to the list of cables
|
if cable.length and cable.length_unit:
|
||||||
link_cables.update({link.pk: cable})
|
description.append(f"{cable.length} {cable.get_length_unit_display()}")
|
||||||
# Add cable to drawing
|
color = cable.color or '000000'
|
||||||
self.connectors.append(cable)
|
|
||||||
|
|
||||||
# Draw fan-ins
|
# Collect all connected nodes to this cable
|
||||||
if len(near_ends) > 1 and fanin:
|
near = [term for term in near_terminations if term.object in cable.a_terminations]
|
||||||
for term in terminations:
|
far = [term for term in far_terminations if term.object in cable.b_terminations]
|
||||||
if term.object.cable == link:
|
if not (near and far):
|
||||||
self.draw_fanin(term, cable)
|
# a and b terminations may be swapped
|
||||||
|
near = [term for term in near_terminations if term.object in cable.b_terminations]
|
||||||
|
far = [term for term in far_terminations if term.object in cable.a_terminations]
|
||||||
|
elif isinstance(cable, WirelessLink):
|
||||||
|
labels = [f"{cable}"] if len(links) > 2 else [f"Wireless {cable}", cable.get_status_display()]
|
||||||
|
if cable.ssid:
|
||||||
|
description.append(f"{cable.ssid}")
|
||||||
|
near = [term for term in near_terminations if term.object == cable.interface_a]
|
||||||
|
far = [term for term in far_terminations if term.object == cable.interface_b]
|
||||||
|
if not (near and far):
|
||||||
|
# a and b terminations may be swapped
|
||||||
|
near = [term for term in near_terminations if term.object == cable.interface_b]
|
||||||
|
far = [term for term in far_terminations if term.object == cable.interface_a]
|
||||||
|
|
||||||
# WirelessLink
|
# Select most-probable start and end position
|
||||||
elif type(link) is WirelessLink:
|
start = near[0].bottom_center
|
||||||
wirelesslink = self.draw_wirelesslink(link)
|
end = far[0].top_center
|
||||||
self.connectors.append(wirelesslink)
|
text_offset = 0
|
||||||
|
|
||||||
# Far end termination(s)
|
if len(near) > 1:
|
||||||
if len(far_ends) > 1:
|
# Handle Fan-In - change start position to be directly below start
|
||||||
if fanout:
|
start = (end[0], start[1] + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
|
||||||
self.cursor += FANOUT_HEIGHT
|
self.draw_fanin(start, near, color)
|
||||||
terminations = self.draw_terminations(far_ends)
|
text_offset -= FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
|
||||||
for term in terminations:
|
elif len(far) > 1:
|
||||||
if hasattr(term.object, 'cable') and link_cables.get(term.object.cable.pk):
|
# Handle Fan-Out - change end position to be directly above end
|
||||||
self.draw_fanout(term, link_cables.get(term.object.cable.pk))
|
end = (start[0], end[1] - FANOUT_HEIGHT - FANOUT_LEG_HEIGHT)
|
||||||
else:
|
self.draw_fanout(end, far, color)
|
||||||
self.draw_terminations(far_ends)
|
text_offset -= FANOUT_HEIGHT
|
||||||
elif far_ends:
|
|
||||||
self.draw_terminations(far_ends)
|
|
||||||
else:
|
|
||||||
# Link is not connected to anything
|
|
||||||
break
|
|
||||||
|
|
||||||
# Far end parent
|
# Create the connector
|
||||||
parent_objects = set(end.parent_object for end in far_ends)
|
connector = Connector(
|
||||||
self.draw_parent_objects(parent_objects)
|
start=start,
|
||||||
|
end=end,
|
||||||
|
color=color,
|
||||||
|
wireless=isinstance(cable, WirelessLink),
|
||||||
|
url=f'{self.base_url}{cable.get_absolute_url()}',
|
||||||
|
text_offset=text_offset,
|
||||||
|
labels=labels,
|
||||||
|
description=description
|
||||||
|
)
|
||||||
|
self.connectors.append(connector)
|
||||||
|
|
||||||
# Render a far-end object not connected via a link (e.g. a ProviderNetwork or Site associated with
|
# Render a far-end object not connected via a link (e.g. a ProviderNetwork or Site associated with
|
||||||
# a CircuitTermination)
|
# a CircuitTermination)
|
||||||
elif far_ends:
|
elif far_ends:
|
||||||
|
|
||||||
# Attachment
|
# Attachment
|
||||||
attachment = self.draw_attachment()
|
attachment = self.draw_attachment()
|
||||||
self.connectors.append(attachment)
|
self.connectors.append(attachment)
|
||||||
|
|
||||||
# Object
|
# Object
|
||||||
self.draw_parent_objects(far_ends)
|
parent_object_nodes = self.draw_parent_objects(far_ends)
|
||||||
|
|
||||||
# Determine drawing size
|
# Determine drawing size
|
||||||
self.drawing = svgwrite.Drawing(
|
self.drawing = svgwrite.Drawing(
|
||||||
|
|
|
@ -51,34 +51,6 @@ def get_cabletermination_row_class(record):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def get_interface_row_class(record):
|
|
||||||
if not record.enabled:
|
|
||||||
return 'danger'
|
|
||||||
elif record.is_virtual:
|
|
||||||
return 'primary'
|
|
||||||
return get_cabletermination_row_class(record)
|
|
||||||
|
|
||||||
|
|
||||||
def get_interface_state_attribute(record):
|
|
||||||
"""
|
|
||||||
Get interface enabled state as string to attach to <tr/> DOM element.
|
|
||||||
"""
|
|
||||||
if record.enabled:
|
|
||||||
return 'enabled'
|
|
||||||
else:
|
|
||||||
return 'disabled'
|
|
||||||
|
|
||||||
|
|
||||||
def get_interface_connected_attribute(record):
|
|
||||||
"""
|
|
||||||
Get interface disconnected state as string to attach to <tr/> DOM element.
|
|
||||||
"""
|
|
||||||
if record.mark_connected or record.cable:
|
|
||||||
return 'connected'
|
|
||||||
else:
|
|
||||||
return 'disconnected'
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
|
@ -706,11 +678,12 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||||
'cable', 'connection',
|
'cable', 'connection',
|
||||||
)
|
)
|
||||||
row_attrs = {
|
row_attrs = {
|
||||||
'class': get_interface_row_class,
|
|
||||||
'data-name': lambda record: record.name,
|
'data-name': lambda record: record.name,
|
||||||
'data-enabled': get_interface_state_attribute,
|
'data-enabled': lambda record: "enabled" if record.enabled else "disabled",
|
||||||
'data-type': lambda record: record.type,
|
'data-virtual': lambda record: "true" if record.is_virtual else "false",
|
||||||
'data-connected': get_interface_connected_attribute
|
'data-mark-connected': lambda record: "true" if record.mark_connected else "false",
|
||||||
|
'data-cable-status': lambda record: record.cable.status if record.cable else "",
|
||||||
|
'data-type': lambda record: record.type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -7,10 +7,10 @@ import { isTruthy, apiPatch, hasError, getElements } from '../util';
|
||||||
*
|
*
|
||||||
* @param element Connection Toggle Button Element
|
* @param element Connection Toggle Button Element
|
||||||
*/
|
*/
|
||||||
function toggleConnection(element: HTMLButtonElement): void {
|
function setConnectionStatus(element: HTMLButtonElement, status: string): void {
|
||||||
|
// Get the button's row to change its data-cable-status attribute
|
||||||
|
const row = element.parentElement?.parentElement as HTMLTableRowElement;
|
||||||
const url = element.getAttribute('data-url');
|
const url = element.getAttribute('data-url');
|
||||||
const connected = element.classList.contains('connected');
|
|
||||||
const status = connected ? 'planned' : 'connected';
|
|
||||||
|
|
||||||
if (isTruthy(url)) {
|
if (isTruthy(url)) {
|
||||||
apiPatch(url, { status }).then(res => {
|
apiPatch(url, { status }).then(res => {
|
||||||
|
@ -19,34 +19,18 @@ function toggleConnection(element: HTMLButtonElement): void {
|
||||||
createToast('danger', 'Error', res.error).show();
|
createToast('danger', 'Error', res.error).show();
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Get the button's row to change its styles.
|
// Update cable status in DOM
|
||||||
const row = element.parentElement?.parentElement as HTMLTableRowElement;
|
row.setAttribute('data-cable-status', status);
|
||||||
// Get the button's icon to change its CSS class.
|
|
||||||
const icon = element.querySelector('i.mdi, span.mdi') as HTMLSpanElement;
|
|
||||||
if (connected) {
|
|
||||||
row.classList.remove('success');
|
|
||||||
row.classList.add('info');
|
|
||||||
element.classList.remove('connected', 'btn-warning');
|
|
||||||
element.classList.add('btn-info');
|
|
||||||
element.title = 'Mark Installed';
|
|
||||||
icon.classList.remove('mdi-lan-disconnect');
|
|
||||||
icon.classList.add('mdi-lan-connect');
|
|
||||||
} else {
|
|
||||||
row.classList.remove('info');
|
|
||||||
row.classList.add('success');
|
|
||||||
element.classList.remove('btn-success');
|
|
||||||
element.classList.add('connected', 'btn-warning');
|
|
||||||
element.title = 'Mark Installed';
|
|
||||||
icon.classList.remove('mdi-lan-connect');
|
|
||||||
icon.classList.add('mdi-lan-disconnect');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initConnectionToggle(): void {
|
export function initConnectionToggle(): void {
|
||||||
for (const element of getElements<HTMLButtonElement>('button.cable-toggle')) {
|
for (const element of getElements<HTMLButtonElement>('button.mark-planned')) {
|
||||||
element.addEventListener('click', () => toggleConnection(element));
|
element.addEventListener('click', () => setConnectionStatus(element, 'planned'));
|
||||||
|
}
|
||||||
|
for (const element of getElements<HTMLButtonElement>('button.mark-installed')) {
|
||||||
|
element.addEventListener('click', () => setConnectionStatus(element, 'connected'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1075,4 +1075,41 @@ html {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Apply row colours to interface lists
|
||||||
|
&[data-netbox-url-name='device_interfaces'] {
|
||||||
|
tr[data-cable-status=connected] {
|
||||||
|
background-color: rgba(map.get($theme-colors, "green"), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr[data-cable-status=planned] {
|
||||||
|
background-color: rgba(map.get($theme-colors, "blue"), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr[data-cable-status=decommissioning] {
|
||||||
|
background-color: rgba(map.get($theme-colors, "yellow"), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr[data-mark-connected=true] {
|
||||||
|
background-color: rgba(map.get($theme-colors, "success"), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr[data-virtual=true] {
|
||||||
|
background-color: rgba(map.get($theme-colors, "primary"), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr[data-enabled=disabled] {
|
||||||
|
background-color: rgba(map.get($theme-colors, "danger"), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show the correct button depending on the cable status
|
||||||
|
tr[data-cable-status=connected] button.mark-installed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:not([data-cable-status=connected]) button.mark-planned {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,9 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% if perms.dcim.change_cable %}
|
{% if perms.dcim.change_cable %}
|
||||||
{% if cable.status == 'connected' %}
|
<button type="button" class="btn btn-warning btn-sm mark-planned" title="{% trans "Mark Planned" %}" data-url="{% url 'dcim-api:cable-detail' pk=cable.pk %}">
|
||||||
<button type="button" class="btn btn-warning btn-sm cable-toggle connected" title="{% trans "Mark Planned" %}" data-url="{% url 'dcim-api:cable-detail' pk=cable.pk %}">
|
<i class="mdi mdi-lan-disconnect" aria-hidden="true"></i>
|
||||||
<i class="mdi mdi-lan-disconnect" aria-hidden="true"></i>
|
</button>
|
||||||
</button>
|
<button type="button" class="btn btn-info btn-sm mark-installed" title="{% trans "Mark Installed" %}" data-url="{% url 'dcim-api:cable-detail' pk=cable.pk %}">
|
||||||
{% else %}
|
<i class="mdi mdi-lan-connect" aria-hidden="true"></i>
|
||||||
<button type="button" class="btn btn-info btn-sm cable-toggle" title="{% trans "Mark Installed" %}" data-url="{% url 'dcim-api:cable-detail' pk=cable.pk %}">
|
</button>
|
||||||
<i class="mdi mdi-lan-connect" aria-hidden="true"></i>
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
Loading…
Reference in New Issue