Compare commits

...

6 Commits

Author SHA1 Message Date
github-actions
c1bbc026e2 Update source translation strings
Some checks are pending
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
2026-01-15 05:05:36 +00:00
Jason Novinger
434334d927 Fixes #20239: Prevent shared mutable state in PluginMenuItem and PluginMenuButton (#21099)
Some checks failed
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
PluginMenuItem and PluginMenuButton classes used mutable class-level
defaults for `permissions` and `buttons` attributes, causing permission
leakage between instances when these attributes were modified without
explicit parameters.

Changed to initialize these attributes as fresh lists per instance in
__init__ when not explicitly provided, following standard Python pattern
for avoiding mutable default arguments.
2026-01-14 12:50:35 -08:00
Jeremy Stretch
6bd083b7ed Closes #21142: Enable filtering device components by site/location/rack directly via GraphQL API (#21145) 2026-01-14 08:06:55 -06:00
bctiemann
f38faf2e01 Merge pull request #21135 from netbox-community/21102-fix-graphiql-explorer
Some checks failed
CI / build (20.x, 3.12) (push) Has been cancelled
CI / build (20.x, 3.13) (push) Has been cancelled
CI / build (20.x, 3.14) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Lock threads / lock (push) Has been cancelled
Close stale issues/PRs / stale (push) Has been cancelled
Close incomplete issues / stale (push) Has been cancelled
Update translation strings / makemessages (push) Has been cancelled
Fixes #21102: Fix GraphiQL explorer UI
2026-01-13 12:33:58 -05:00
Mark Robert Coleman
e60807adc5 Fixes #21121: Expand changelog message doc/add cross-references (#21138) 2026-01-13 09:58:06 -06:00
Jeremy Stretch
c3e111c769 Fixes #21102: Fix GraphiQL explorer UI 2026-01-12 14:34:17 -05:00
10 changed files with 76 additions and 21 deletions

View File

@@ -10,9 +10,11 @@ Change records are exposed in the API via the read-only endpoint `/api/extras/ob
## User Messages ## User Messages
!!! info "This feature was introduced in NetBox v4.4." When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message (up to 200 characters) that will appear in the change record. This can be helpful to capture additional context, such as the reason for a change or a reference to an external ticket.
When creating, modifying, or deleting an object in NetBox, a user has the option of recording an arbitrary message that will appear in the change record. This can be helpful to capture additional context, such as the reason for the change. When editing an object via the web UI, the "Changelog message" field appears at the bottom of the form. This field is optional. The changelog message field is available in object create forms, object edit forms, delete confirmation dialogs, and bulk operations.
For information on including changelog messages when making changes via the REST API, see [Changelog Messages](../integrations/rest-api.md#changelog-messages).
## Correlating Changes by Request ## Correlating Changes by Request

View File

@@ -610,9 +610,7 @@ http://netbox/api/dcim/sites/ \
## Changelog Messages ## Changelog Messages
!!! info "This feature was introduced in NetBox v4.4." Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Additionally, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
Most objects in NetBox support [change logging](../features/change-logging.md), which generates a detailed record each time an object is created, modified, or deleted. Beginning in NetBox v4.4, users can attach a message to the change record as well. This is accomplished via the REST API by including a `changelog_message` field in the object representation.
For example, the following API request will create a new site and record a message in the resulting changelog entry: For example, the following API request will create a new site and record a message in the resulting changelog entry:
@@ -628,7 +626,7 @@ http://netbox/api/dcim/sites/ \
}' }'
``` ```
This approach works when creating, modifying, or deleting objects, either individually or in bulk. This approach works when creating, modifying, or deleting objects, either individually or in bulk. For more information about change logging, see [Change Logging](../features/change-logging.md).
## Uploading Files ## Uploading Files

View File

@@ -38,6 +38,15 @@ class ScopedFilterMixin:
@dataclass @dataclass
class ComponentModelFilterMixin: class ComponentModelFilterMixin:
_site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='site')
)
_location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='location')
)
_rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = (
strawberry_django.filter_field(name='rack')
)
device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field()
device_id: ID | None = strawberry_django.filter_field() device_id: ID | None = strawberry_django.filter_field()
name: FilterLookup[str] | None = strawberry_django.filter_field() name: FilterLookup[str] | None = strawberry_django.filter_field()

View File

@@ -37,8 +37,6 @@ class PluginMenuItem:
Alternatively, a pre-generated url can be set on the object which will be rendered literally. Alternatively, a pre-generated url can be set on the object which will be rendered literally.
Buttons are each specified as a list of PluginMenuButton instances. Buttons are each specified as a list of PluginMenuButton instances.
""" """
permissions = []
buttons = []
_url = None _url = None
def __init__( def __init__(
@@ -54,10 +52,14 @@ class PluginMenuItem:
if type(permissions) not in (list, tuple): if type(permissions) not in (list, tuple):
raise TypeError(_("Permissions must be passed as a tuple or list.")) raise TypeError(_("Permissions must be passed as a tuple or list."))
self.permissions = permissions self.permissions = permissions
else:
self.permissions = []
if buttons is not None: if buttons is not None:
if type(buttons) not in (list, tuple): if type(buttons) not in (list, tuple):
raise TypeError(_("Buttons must be passed as a tuple or list.")) raise TypeError(_("Buttons must be passed as a tuple or list."))
self.buttons = buttons self.buttons = buttons
else:
self.buttons = []
@property @property
def url(self): def url(self):
@@ -74,7 +76,6 @@ class PluginMenuButton:
ButtonColorChoices. ButtonColorChoices.
""" """
color = ButtonColorChoices.DEFAULT color = ButtonColorChoices.DEFAULT
permissions = []
_url = None _url = None
def __init__(self, link, title, icon_class, color=None, permissions=None): def __init__(self, link, title, icon_class, color=None, permissions=None):
@@ -87,6 +88,8 @@ class PluginMenuButton:
if type(permissions) not in (list, tuple): if type(permissions) not in (list, tuple):
raise TypeError(_("Permissions must be passed as a tuple or list.")) raise TypeError(_("Permissions must be passed as a tuple or list."))
self.permissions = permissions self.permissions = permissions
else:
self.permissions = []
if color is not None: if color is not None:
if color not in ButtonColorChoices.values(): if color not in ButtonColorChoices.values():
raise ValueError(_("Button color must be a choice within ButtonColorChoices.")) raise ValueError(_("Button color must be a choice within ButtonColorChoices."))

View File

@@ -11,7 +11,7 @@ from netbox.tests.dummy_plugin import config as dummy_config
from netbox.tests.dummy_plugin.data_backends import DummyBackend from netbox.tests.dummy_plugin.data_backends import DummyBackend
from netbox.tests.dummy_plugin.jobs import DummySystemJob from netbox.tests.dummy_plugin.jobs import DummySystemJob
from netbox.tests.dummy_plugin.webhook_callbacks import set_context from netbox.tests.dummy_plugin.webhook_callbacks import set_context
from netbox.plugins.navigation import PluginMenu from netbox.plugins.navigation import PluginMenu, PluginMenuItem, PluginMenuButton
from netbox.plugins.utils import get_plugin_config from netbox.plugins.utils import get_plugin_config
from netbox.graphql.schema import Query from netbox.graphql.schema import Query
from netbox.registry import registry from netbox.registry import registry
@@ -227,3 +227,46 @@ class PluginTest(TestCase):
Test the registration of webhook callbacks. Test the registration of webhook callbacks.
""" """
self.assertIn(set_context, registry['webhook_callbacks']) self.assertIn(set_context, registry['webhook_callbacks'])
class PluginNavigationTest(TestCase):
def test_plugin_menu_item_independent_permissions(self):
item1 = PluginMenuItem(link='test1', link_text='Test 1')
item1.permissions.append('leaked_permission')
item2 = PluginMenuItem(link='test2', link_text='Test 2')
self.assertIsNot(item1.permissions, item2.permissions)
self.assertEqual(item1.permissions, ['leaked_permission'])
self.assertEqual(item2.permissions, [])
def test_plugin_menu_item_independent_buttons(self):
item1 = PluginMenuItem(link='test1', link_text='Test 1')
button = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
item1.buttons.append(button)
item2 = PluginMenuItem(link='test2', link_text='Test 2')
self.assertIsNot(item1.buttons, item2.buttons)
self.assertEqual(len(item1.buttons), 1)
self.assertEqual(item1.buttons[0], button)
self.assertEqual(item2.buttons, [])
def test_plugin_menu_button_independent_permissions(self):
button1 = PluginMenuButton(link='button1', title='Button 1', icon_class='mdi-test')
button1.permissions.append('leaked_permission')
button2 = PluginMenuButton(link='button2', title='Button 2', icon_class='mdi-test')
self.assertIsNot(button1.permissions, button2.permissions)
self.assertEqual(button1.permissions, ['leaked_permission'])
self.assertEqual(button2.permissions, [])
def test_explicit_permissions_remain_independent(self):
item1 = PluginMenuItem(link='test1', link_text='Test 1', permissions=['explicit_permission'])
item2 = PluginMenuItem(link='test2', link_text='Test 2', permissions=['different_permission'])
self.assertIsNot(item1.permissions, item2.permissions)
self.assertEqual(item1.permissions, ['explicit_permission'])
self.assertEqual(item2.permissions, ['different_permission'])

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
.docExplorerWrap{height:unset!important;min-width:unset!important;width:unset!important}.docExplorerWrap svg{display:unset}.doc-explorer-title{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium)}.doc-explorer-rhs{display:none}.graphiql-explorer-root{font-family:var(--font-family-mono)!important;font-size:var(--font-size-body)!important;padding:0!important}.graphiql-explorer-root>div>div{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important;padding-top:var(--px-16)}.graphiql-explorer-root input{background:unset}.graphiql-explorer-root select{background:hsl(var(--color-base))!important;border:1px solid hsla(var(--color-neutral),var(--alpha-secondary));border-radius:var(--border-radius-4);color:hsl(var(--color-neutral))!important;margin:0 var(--px-8);padding:var(--px-4) var(--px-6)}.graphiql-operation-title-bar .toolbar-button{line-height:0;margin-left:var(--px-8);color:hsla(var(--color-neutral),var(--alpha-secondary, .6));font-size:var(--font-size-h3);vertical-align:middle}.graphiql-explorer-graphql-arguments input{line-height:0}.graphiql-explorer-actions{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important} .docExplorerWrap{height:unset!important;min-width:unset!important;width:unset!important}.docExplorerWrap svg{display:unset}.doc-explorer-title{font-size:var(--font-size-h2);font-weight:var(--font-weight-medium)}.doc-explorer-rhs{display:none}.graphiql-explorer-root{font-family:var(--font-family-mono)!important;font-size:var(--font-size-body)!important;padding:0!important}.graphiql-explorer-root>div>div{padding-top:var(--px-16);border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important}.graphiql-explorer-root>div{overflow:auto!important}.graphiql-explorer-root input{background:unset}.graphiql-explorer-root select{border:1px solid hsla(var(--color-neutral),var(--alpha-secondary));border-radius:var(--border-radius-4);margin:0 var(--px-8);padding:var(--px-4)var(--px-6);background:hsl(var(--color-base))!important;color:hsl(var(--color-neutral))!important}.toolbar-button{all:unset;cursor:pointer;margin-left:var(--px-6);color:hsl(var(--color-primary));line-height:0!important;font-size:var(--font-size-h3)!important}.graphiql-explorer-slug .toolbar-button,.graphiql-explorer-graphql-arguments .toolbar-button{font-size:inherit!important}.graphiql-explorer-graphql-arguments input{min-width:2rem;line-height:0}.graphiql-explorer-actions{border-color:hsla(var(--color-neutral),var(--alpha-background-heavy))!important}

View File

@@ -6,7 +6,7 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@graphiql/plugin-explorer": "3.2.6", "@graphiql/plugin-explorer": "4.0.6",
"graphiql": "4.1.2", "graphiql": "4.1.2",
"graphql": "16.12.0", "graphql": "16.12.0",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",

View File

@@ -294,10 +294,10 @@
react-compiler-runtime "19.1.0-rc.1" react-compiler-runtime "19.1.0-rc.1"
zustand "^5" zustand "^5"
"@graphiql/plugin-explorer@3.2.6": "@graphiql/plugin-explorer@4.0.6":
version "3.2.6" version "4.0.6"
resolved "https://registry.npmjs.org/@graphiql/plugin-explorer/-/plugin-explorer-3.2.6.tgz" resolved "https://registry.yarnpkg.com/@graphiql/plugin-explorer/-/plugin-explorer-4.0.6.tgz#bec1207dc27334914590ab31f46c2e944bbf4ebf"
integrity sha512-MXzG/zVNzZfes4Em253bHyAbD/lwwAZkPKvxCAQkjz0i3dtcv4uF3D8iqJ7214iu3SCphbORYZZUC93fik1yew== integrity sha512-TppIi92YPER3v70nlF01KTQrq9AiYqkZicSd1hpU7aqGmbqw/pLwBNLUEcfENBoJtw574Qxjswb01+GaYK0Tzw==
dependencies: dependencies:
graphiql-explorer "^0.9.0" graphiql-explorer "^0.9.0"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-13 05:05+0000\n" "POT-Creation-Date: 2026-01-15 05:05+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -12489,8 +12489,8 @@ msgstr ""
msgid "Delete Selected" msgid "Delete Selected"
msgstr "" msgstr ""
#: netbox/netbox/plugins/navigation.py:55 #: netbox/netbox/plugins/navigation.py:53
#: netbox/netbox/plugins/navigation.py:88 #: netbox/netbox/plugins/navigation.py:89
msgid "Permissions must be passed as a tuple or list." msgid "Permissions must be passed as a tuple or list."
msgstr "" msgstr ""
@@ -12498,7 +12498,7 @@ msgstr ""
msgid "Buttons must be passed as a tuple or list." msgid "Buttons must be passed as a tuple or list."
msgstr "" msgstr ""
#: netbox/netbox/plugins/navigation.py:92 #: netbox/netbox/plugins/navigation.py:95
msgid "Button color must be a choice within ButtonColorChoices." msgid "Button color must be a choice within ButtonColorChoices."
msgstr "" msgstr ""