Compare commits

...

26 Commits

Author SHA1 Message Date
Ash Kirby 01a4c91413
Merge 10c6862735 into 85db007ff5 2024-04-26 13:36:57 -03:00
Daniel Sheppard 85db007ff5
Update changelog for #14750 2024-04-22 21:57:40 -05:00
Daniel Sheppard cad3e34d8f
Merge pull request #14750 from Moehritz/13922-svg-uneven
Fixes #14241, Fixes #13922: Update the CableRender
2024-04-22 21:53:34 -05:00
Daniel Sheppard 7b1b91b8ee
Correct wording for #13874 2024-04-22 21:51:54 -05:00
Daniel Sheppard 6f36b8513c
Update changelog for #13874 2024-04-22 21:51:08 -05:00
Daniel Sheppard 07e2cf0ad2
Merge pull request #13874 from pv2b/choices-css-rewrite
Refactor row coloring logic and simplify mark planned/connected toggle implementation
2024-04-22 21:45:15 -05:00
arcticash 10c6862735
Moving the Molex connectors into their own category for better UX - expansion on #12984 2024-03-15 15:55:23 +00:00
Ash Kirby fb646dd915
Merge branch 'netbox-community:develop' into develop 2024-03-15 15:35:29 +00:00
Ash Kirby 6c06d31a80
Merge branch 'netbox-community:develop' into develop 2024-03-11 22:17:04 +00:00
Ash Kirby 2d7e33df3b
Merge branch 'netbox-community:develop' into develop 2024-02-18 20:07:54 +00:00
Ash Kirby f613fe7650
Merge branch 'netbox-community:develop' into develop 2024-02-06 21:58:29 +00:00
arcticash f49dcbe9d4
Adding Molex Micro-Fit connectors to power outlet choices to fix #12984 2024-01-23 21:53:35 +00:00
arcticash ad58d370d2
Adding Molex Micro-Fit connectors to power plug choices to fix #12984 2024-01-23 21:47:10 +00:00
Per von Zweigbergk 8fadd6b744 Merge branch 'develop' into choices-css-rewrite 2024-01-23 21:50:06 +01:00
Per von Zweigbergk c93413dc9c Move interface colour logic into SCSS where it belongs 2024-01-23 21:33:09 +01:00
Per von Zweigbergk bf362f4679 Hardcode cable status colours 2024-01-23 20:58:10 +01:00
Per von Zweigbergk da7f67c359 Refactor noisy getter methods into neat lambdas 2024-01-23 20:49:10 +01:00
Moritz Geist 2c93dd03e1 account for swapped terminations in cable object
also remove out-of-scope changes to tooltips
2024-01-10 14:29:46 +01:00
Moritz Geist ced44832f7 Remove dangling logging message used during development 2024-01-09 14:22:36 +01:00
Moritz Geist 6af3aad362 Fixes #14722, Fixes #13922: Update the CableRender
This commit updates the cable rendering logic to fix
both issue #14722 and #13922. Before, objects, terminations
and cables where drawn in the svg without context of each
other.
Now the following changes are applied:
- Hosts and Terminations are where possible sorted alphabetically
- Terminations and Cables are visually connected, and if necessary not in a vertical line
- Terminations and Hosts are visually aligning
- Cable Tooltips contain more information
2024-01-09 13:51:09 +01:00
Per von Zweigbergk c728d3c2e8 Fix formatting 2023-09-24 00:08:39 +02:00
Per von Zweigbergk 83e2c45e74 Simplify mark connected/installed implementation
Fixes: #13712 and #13806.
2023-09-23 23:45:08 +02:00
Per von Zweigbergk 27864ec865 Move DeviceInterfaceTable coloring logic into CSS
Preparatory work for simplifying toggle button code for cable status.
2023-09-23 23:07:16 +02:00
Per von Zweigbergk d44f67aea5 Add 15% alpha variants of --nbx-color
Preparatory work for factoring row styling out of Python
2023-09-23 23:01:08 +02:00
Per von Zweigbergk 41e1f24cf7 Add --nbx-color-* variables for theme colors
Preparatory work for moving row styling to CSS
2023-09-23 21:43:32 +02:00
Per von Zweigbergk d76ede17d3 Add data properties for device interface table
Preparatory work for factoring row styling decisions out of Python code.
2023-09-23 21:33:47 +02:00
12 changed files with 265 additions and 270 deletions

View File

@ -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)

View File

@ -399,6 +399,10 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_USB_MICRO_AB = 'usb-micro-ab' TYPE_USB_MICRO_AB = 'usb-micro-ab'
TYPE_USB_3_B = 'usb-3-b' TYPE_USB_3_B = 'usb-3-b'
TYPE_USB_3_MICROB = 'usb-3-micro-b' TYPE_USB_3_MICROB = 'usb-3-micro-b'
# Molex
TYPE_MOLEX_MICRO_FIT_1X2 = 'molex-micro-fit-1x2'
TYPE_MOLEX_MICRO_FIT_2X2 = 'molex-micro-fit-2x2'
TYPE_MOLEX_MICRO_FIT_2X4 = 'molex-micro-fit-2x4'
# Direct current (DC) # Direct current (DC)
TYPE_DC = 'dc-terminal' TYPE_DC = 'dc-terminal'
# Proprietary # Proprietary
@ -520,6 +524,11 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_USB_3_B, 'USB 3.0 Type B'), (TYPE_USB_3_B, 'USB 3.0 Type B'),
(TYPE_USB_3_MICROB, 'USB 3.0 Micro B'), (TYPE_USB_3_MICROB, 'USB 3.0 Micro B'),
)), )),
('Molex', (
(TYPE_MOLEX_MICRO_FIT_1X2, 'Molex Micro-Fit 1x2'),
(TYPE_MOLEX_MICRO_FIT_2X2, 'Molex Micro-Fit 2x2'),
(TYPE_MOLEX_MICRO_FIT_2X4, 'Molex Micro-Fit 2x4'),
)),
('DC', ( ('DC', (
(TYPE_DC, 'DC Terminal'), (TYPE_DC, 'DC Terminal'),
)), )),
@ -635,6 +644,10 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_USB_A = 'usb-a' TYPE_USB_A = 'usb-a'
TYPE_USB_MICROB = 'usb-micro-b' TYPE_USB_MICROB = 'usb-micro-b'
TYPE_USB_C = 'usb-c' TYPE_USB_C = 'usb-c'
# Molex
TYPE_MOLEX_MICRO_FIT_1X2 = 'molex-micro-fit-1x2'
TYPE_MOLEX_MICRO_FIT_2X2 = 'molex-micro-fit-2x2'
TYPE_MOLEX_MICRO_FIT_2X4 = 'molex-micro-fit-2x4'
# Direct current (DC) # Direct current (DC)
TYPE_DC = 'dc-terminal' TYPE_DC = 'dc-terminal'
# Proprietary # Proprietary
@ -749,6 +762,11 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_USB_MICROB, 'USB Micro B'), (TYPE_USB_MICROB, 'USB Micro B'),
(TYPE_USB_C, 'USB Type C'), (TYPE_USB_C, 'USB Type C'),
)), )),
('Molex', (
(TYPE_MOLEX_MICRO_FIT_1X2, 'Molex Micro-Fit 1x2'),
(TYPE_MOLEX_MICRO_FIT_2X2, 'Molex Micro-Fit 2x2'),
(TYPE_MOLEX_MICRO_FIT_2X4, 'Molex Micro-Fit 2x4'),
)),
('DC', ( ('DC', (
(TYPE_DC, 'DC Terminal'), (TYPE_DC, 'DC Terminal'),
)), )),

View File

@ -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(

View File

@ -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

View File

@ -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'));
} }
} }

View File

@ -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;
}
}
}

View File

@ -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 %}