Compare commits

..

11 Commits

Author SHA1 Message Date
Jeremy Stretch
be903a64a2 Release v3.7.8 2024-05-06 12:54:53 -04:00
transifex-integration[bot]
0d7bac433e Translate django.po in ja
100% translated source file: 'django.po'
on 'ja'.
2024-05-06 12:54:53 -04:00
Jeremy Stretch
b1cfbbc472 Fixes #15960: Use internal ManyToManyColumn to ensure proper export behavior 2024-05-06 12:54:53 -04:00
Jeremy Stretch
6dd311f600 Fixes #15961: Fix secret toggle button by avoiding duplicate event handler 2024-05-06 12:54:53 -04:00
Daniel Sheppard
85d250014f Fixes: #15948 - Fixes cable fanin/fanout when both are required (#15953)
* Preliminary fix for #15948

* Tweaking of line height
2024-05-06 12:54:53 -04:00
Arthur
552c81509a 12127 enable cable add button 2024-05-06 12:54:53 -04:00
Jeremy Stretch
ed7a0a32cc Changelog for #15877, #15917, #15925 2024-05-06 12:54:53 -04:00
Nancy Yang
a544b55e9e Fixes #15917: slim-select-pagination-bug-fix : fixed several bugs related to slim select (#15918)
* slim-select-pagination-bug-fix : fixed several bugs related to slim
select search box gui element
1. If user enters a search text in the filter text box, the user will
   not be able to scroll to the next page. That is the user will only be
   able to see the first page of returned item with a none empty search
   string.
2. User will not be able to select an item returned from search query
   if user clicks reload after a dynami search. When the user is able
   to load a second page, the user will be able to select an item from
   the third+ page if previous bug is fixed.

* Recompile static assets

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-05-06 12:54:53 -04:00
Jeremy Stretch
53e1ab5fc5 Fixes #15877: Consider VC membership when assigning LAG interfaces via bulk edit 2024-05-06 12:54:53 -04:00
Jeremy Stretch
2c1a9ae455 Fixes #15925: Fix rendering of cable traces to circuit terminations 2024-05-06 12:54:53 -04:00
Jeremy Stretch
1afa476a19 PRVB 2024-05-06 12:54:53 -04:00
20 changed files with 438 additions and 270 deletions

View File

@@ -26,7 +26,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v3.7.7
placeholder: v3.7.8
validations:
required: true
- type: dropdown

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.7.7
placeholder: v3.7.8
validations:
required: true
- type: dropdown

View File

@@ -1,5 +1,22 @@
# NetBox v3.7
## v3.7.8 (2024-05-06)
### Enhancements
* [#12127](https://github.com/netbox-community/netbox/issues/12127) - Enable adding new cables directly from navigation menu
### Bug Fixes
* [#15877](https://github.com/netbox-community/netbox/issues/15877) - Account for virtual chassis membership when assigning related interfaces via bulk edit
* [#15917](https://github.com/netbox-community/netbox/issues/15917) - Fix pagination through search results within dropdown fields
* [#15925](https://github.com/netbox-community/netbox/issues/15925) - Fix SVG rendering of cable traces to circuit terminations
* [#15948](https://github.com/netbox-community/netbox/issues/15948) - Fix cable trace SVG generation for cables with multiple terminations at both ends
* [#15960](https://github.com/netbox-community/netbox/issues/15960) - Replace CSV export formatting for several many-to-many fields
* [#15961](https://github.com/netbox-community/netbox/issues/15961) - Fix secret toggle button for IKE policies
---
## v3.7.7 (2024-05-01)
### Enhancements

View File

@@ -1411,9 +1411,9 @@ class InterfaceBulkEditForm(
device = Device.objects.filter(pk=self.initial['device']).first()
# Restrict parent/bridge/LAG interface assignment by device
self.fields['parent'].widget.add_query_param('device_id', device.pk)
self.fields['bridge'].widget.add_query_param('device_id', device.pk)
self.fields['lag'].widget.add_query_param('device_id', device.pk)
self.fields['parent'].widget.add_query_param('virtual_chassis_member_id', device.pk)
self.fields['bridge'].widget.add_query_param('virtual_chassis_member_id', device.pk)
self.fields['lag'].widget.add_query_param('virtual_chassis_member_id', device.pk)
# Limit VLAN choices by device
self.fields['untagged_vlan'].widget.add_query_param('available_on_device', device.pk)

View File

@@ -17,7 +17,7 @@ PADDING = 10
LINE_HEIGHT = 20
FANOUT_HEIGHT = 35
FANOUT_LEG_HEIGHT = 15
CABLE_HEIGHT = 4 * LINE_HEIGHT + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
CABLE_HEIGHT = 5 * LINE_HEIGHT + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT
class Node(Hyperlink):
@@ -223,7 +223,7 @@ class CableTraceSVG:
nodes_height = 0
nodes = []
# Sort them by name to make renders more readable
for i, term in enumerate(sorted(terminations, key=lambda x: x.name)):
for i, term in enumerate(sorted(terminations, key=lambda x: str(x))):
node = Node(
position=(offset_x + i * width, self.cursor),
width=width,
@@ -266,7 +266,7 @@ class CableTraceSVG:
Draw the far-end objects and its terminations and return all created nodes
"""
# Make sure elements are sorted by name for readability
objects = sorted(obj_list, key=lambda x: x.name)
objects = sorted(obj_list, key=lambda x: str(x))
width = self.width / len(objects)
# Max-height of created terminations
@@ -361,7 +361,8 @@ class CableTraceSVG:
# Connector (a Cable or WirelessLink)
if links:
parent_object_nodes, far_terminations = self.draw_far_objects(set(end.parent_object for end in far_ends), far_ends)
obj_list = {end.parent_object for end in far_ends}
parent_object_nodes, far_terminations = self.draw_far_objects(obj_list, far_ends)
for cable in links:
# Fill in labels and description with all available data
description = [
@@ -404,7 +405,17 @@ class CableTraceSVG:
end = far[0].top_center
text_offset = 0
if len(near) > 1:
if len(near) > 1 and len(far) > 1:
start_center = sum([pos.bottom_center[0] for pos in near]) / len(near)
end_center = sum([pos.bottom_center[0] for pos in far]) / len(far)
center_x = (start_center + end_center) / 2
start = (center_x, start[1] + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
end = (center_x, end[1] - FANOUT_HEIGHT - FANOUT_LEG_HEIGHT)
text_offset -= (FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
self.draw_fanin(start, near, color)
self.draw_fanout(end, far, color)
elif len(near) > 1:
# Handle Fan-In - change start position to be directly below start
start = (end[0], start[1] + FANOUT_HEIGHT + FANOUT_LEG_HEIGHT)
self.draw_fanin(start, near, color)

View File

@@ -618,7 +618,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
verbose_name=_('VRF'),
linkify=True
)
inventory_items = tables.ManyToManyColumn(
inventory_items = columns.ManyToManyColumn(
linkify_item=True,
verbose_name=_('Inventory Items'),
)

View File

@@ -394,6 +394,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 2)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 2
cable2.delete()
path1 = self.assertPathExists(
@@ -450,6 +453,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 2)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 2
cable2.delete()
path1 = self.assertPathExists(
@@ -558,6 +564,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 4)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 3
cable3.delete()
@@ -673,6 +682,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 4)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 3
cable3.delete()
@@ -804,6 +816,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 4)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 3
cable3.delete()
@@ -931,6 +946,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 4)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 5
cable5.delete()
@@ -1034,6 +1052,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 4)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 3
cable3.delete()
@@ -1093,6 +1114,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 3)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 1
cable1.delete()
@@ -1135,6 +1159,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 1)
# Test SVG generation
CableTraceSVG(interface1).render()
def test_210_interface_to_circuittermination(self):
"""
[IF1] --C1-- [CT1]
@@ -1156,6 +1183,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 1)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 1
cable1.delete()
self.assertEqual(CablePath.objects.count(), 0)
@@ -1212,6 +1242,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 2)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 2
cable2.delete()
path1 = self.assertPathExists(
@@ -1277,6 +1310,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 2)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 2
cable2.delete()
path1 = self.assertPathExists(
@@ -1314,6 +1350,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 1)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 1
cable1.delete()
self.assertEqual(CablePath.objects.count(), 0)
@@ -1342,6 +1381,9 @@ class CablePathTestCase(TestCase):
self.assertEqual(CablePath.objects.count(), 1)
self.assertTrue(CablePath.objects.first().is_complete)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 1
cable1.delete()
self.assertEqual(CablePath.objects.count(), 0)
@@ -1439,6 +1481,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 4)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cables 3-4
cable3.delete()
cable4.delete()
@@ -1495,6 +1540,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 2)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 2
cable2.delete()
path1 = self.assertPathExists(
@@ -1578,6 +1626,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 2)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 2
cable2.delete()
@@ -1697,6 +1748,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 4)
# Test SVG generation
CableTraceSVG(interface1).render()
# Delete cable 3
cable3.delete()
@@ -1784,6 +1838,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 2)
# Test SVG generation
CableTraceSVG(interface1).render()
def test_220_interface_to_interface_duplex_via_multiple_front_and_rear_ports(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
@@ -1877,6 +1934,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 3)
# Test SVG generation
CableTraceSVG(interface1).render()
def test_221_non_symmetric_paths(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- -------------------------------------- [IF2]
@@ -1997,6 +2057,9 @@ class CablePathTestCase(TestCase):
)
self.assertEqual(CablePath.objects.count(), 3)
# Test SVG generation
CableTraceSVG(interface1).render()
def test_301_create_path_via_existing_cable(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]

View File

@@ -3166,12 +3166,6 @@ class CableListView(generic.ObjectListView):
filterset = filtersets.CableFilterSet
filterset_form = forms.CableFilterForm
table = tables.CableTable
actions = {
'import': {'add'},
'export': {'view'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
@register_model_view(Cable)

View File

@@ -378,7 +378,7 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
orderable=False,
verbose_name=_('NAT (Inside)')
)
nat_outside = tables.ManyToManyColumn(
nat_outside = columns.ManyToManyColumn(
linkify_item=True,
orderable=False,
verbose_name=_('NAT (Outside)')

View File

@@ -102,7 +102,7 @@ CONNECTIONS_MENU = Menu(
MenuGroup(
label=_('Connections'),
items=(
get_model_item('dcim', 'cable', _('Cables'), actions=['import']),
get_model_item('dcim', 'cable', _('Cables')),
get_model_item('wireless', 'wirelesslink', _('Wireless Links')),
MenuItem(
link='dcim:interface_connections_list',

View File

@@ -28,7 +28,7 @@ from netbox.plugins import PluginConfig
# Environment setup
#
VERSION = '3.7.7'
VERSION = '3.7.8'
# Hostname
HOSTNAME = platform.node()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -60,18 +60,17 @@ function handleSecretToggle(state: StateManager<SecretState>, button: HTMLButton
toggleSecretButton(hidden, button);
}
function toggleCallback(event: MouseEvent) {
handleSecretToggle(secretState, event.currentTarget as HTMLButtonElement);
}
/**
* Initialize secret toggle button.
*/
export function initSecretToggle(): void {
hideSecret();
for (const button of getElements<HTMLButtonElement>('button.toggle-secret')) {
button.addEventListener(
'click',
event => {
handleSecretToggle(secretState, event.currentTarget as HTMLButtonElement);
},
false,
);
button.removeEventListener('click', toggleCallback);
button.addEventListener('click', toggleCallback);
}
}

View File

@@ -140,6 +140,10 @@ export class APISelect {
*/
private queryUrl: string = '';
/**
* Interal state variable used to remember search key entered by user for "Filter" search box
*/
private searchKey: Nullable<string> = null;
/**
* Scroll position of options is at the bottom of the list, or not. Used to determine if
* additional options should be fetched from the API.
@@ -359,30 +363,41 @@ export class APISelect {
this.slim.enable();
}
private setSearchKey(event: Event) {
const { value: q } = event.target as HTMLInputElement;
this.searchKey = q
}
/**
* Add event listeners to this element and its dependencies so that when dependencies change
* this element's options are updated.
*/
private addEventListeners(): void {
// Create a debounced function to fetch options based on the search input value.
const fetcher = debounce((event: Event) => this.handleSearch(event), 300, false);
const fetcher = debounce((action:ApplyMethod, url: Nullable<string>) => this.handleSearch(action, url), 300, false);
// Query the API when the input value changes or a value is pasted.
this.slim.slim.search.input.addEventListener('keyup', event => {
// Only search when necessary keys are pressed.
if (!event.key.match(/^(Arrow|Enter|Tab).*/)) {
return fetcher(event);
this.setSearchKey(event);
return fetcher('replace', null);
}
});
this.slim.slim.search.input.addEventListener('paste', event => fetcher(event));
this.slim.slim.search.input.addEventListener('paste', event => {
this.setSearchKey(event);
return fetcher('replace', null);;
});
// Watch every scroll event to determine if the scroll position is at bottom.
this.slim.slim.list.addEventListener('scroll', () => this.handleScroll());
// When the scroll position is at bottom, fetch additional options.
this.base.addEventListener(`netbox.select.atbottom.${this.name}`, () =>
this.fetchOptions(this.more, 'merge'),
);
this.base.addEventListener(`netbox.select.atbottom.${this.name}`, () => {
if (this.more!=null) {
return fetcher('merge', this.more, )
}
});
// When the base select element is disabled or enabled, properly disable/enable this instance.
this.base.addEventListener(`netbox.select.disabled.${this.name}`, event =>
@@ -551,6 +566,14 @@ export class APISelect {
}
}
private getUrl() {
var url = this.queryUrl
if (this.searchKey!=null) {
url = queryString.stringifyUrl({ url: this.queryUrl, query: { q : this.searchKey } })
}
return url
}
/**
* Query the NetBox API for this element's options.
*/
@@ -559,21 +582,25 @@ export class APISelect {
this.resetOptions();
return;
}
await this.fetchOptions(this.queryUrl, action);
const url = this.getUrl()
await this.fetchOptions(url, action);
}
/**
* Query the API for a specific search pattern and add the results to the available options.
*/
private async handleSearch(event: Event) {
const { value: q } = event.target as HTMLInputElement;
const url = queryString.stringifyUrl({ url: this.queryUrl, query: { q } });
if (!url.includes(`{{`)) {
await this.fetchOptions(url, 'merge');
this.slim.data.search(q);
this.slim.render();
private async handleSearch(action: ApplyMethod = 'merge', url: Nullable<string> ) {
if (url==null) {
url = this.getUrl()
}
return;
if (url.includes(`{{`)) {
return
}
await this.fetchOptions(url, action);
if (this.searchKey!=null) {
this.slim.data.search(this.searchKey);
}
this.slim.render();
}
/**
@@ -586,13 +613,11 @@ export class APISelect {
Math.floor(this.slim.slim.list.scrollTop) + this.slim.slim.list.offsetHeight ===
this.slim.slim.list.scrollHeight;
if (this.atBottom && !atBottom) {
this.atBottom = false;
this.atBottom = atBottom
if (this.atBottom) {
this.base.dispatchEvent(this.bottomEvent);
} else if (!this.atBottom && atBottom) {
this.atBottom = true;
this.base.dispatchEvent(this.bottomEvent);
}
}
}
/**
@@ -994,7 +1019,9 @@ export class APISelect {
['btn', 'btn-sm', 'btn-ghost-dark'],
[createElement('i', null, ['mdi', 'mdi-reload'])],
);
refreshButton.addEventListener('click', () => this.loadData());
// calling this.loadData() will prevent first page of returned items
// with non-null search key inplace not selectable
refreshButton.addEventListener('click', () => this.handleSearch('replace', null));
refreshButton.type = 'button';
this.slim.slim.search.container.appendChild(refreshButton);
}

File diff suppressed because it is too large Load Diff

View File

@@ -63,7 +63,7 @@ class IKEPolicyTable(NetBoxTable):
mode = tables.Column(
verbose_name=_('Mode')
)
proposals = tables.ManyToManyColumn(
proposals = columns.ManyToManyColumn(
linkify_item=True,
verbose_name=_('Proposals')
)
@@ -129,7 +129,7 @@ class IPSecPolicyTable(NetBoxTable):
verbose_name=_('Name'),
linkify=True
)
proposals = tables.ManyToManyColumn(
proposals = columns.ManyToManyColumn(
linkify_item=True,
verbose_name=_('Proposals')
)

View File

@@ -91,7 +91,7 @@ class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('Tunnel interface'),
linkify=True
)
ip_addresses = tables.ManyToManyColumn(
ip_addresses = columns.ManyToManyColumn(
accessor=tables.A('termination__ip_addresses'),
orderable=False,
linkify_item=True,

View File

@@ -19,10 +19,10 @@ drf-spectacular-sidecar==2024.5.1
feedparser==6.0.11
graphene-django==3.0.0
gunicorn==22.0.0
Jinja2==3.1.3
Jinja2==3.1.4
Markdown==3.6
mkdocs-material==9.5.20
mkdocstrings[python-legacy]==0.25.0
mkdocs-material==9.5.21
mkdocstrings[python-legacy]==0.25.1
netaddr==1.2.1
Pillow==10.3.0
psycopg[binary,pool]==3.1.18