diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 3d2038b22..5cfc8684d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -26,7 +26,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v4.0.3 + placeholder: v4.0.5 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index bd9a17ff9..c351ec599 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v4.0.3 + placeholder: v4.0.5 validations: required: true - type: dropdown diff --git a/contrib/generated_schema.json b/contrib/generated_schema.json index 5cfdfd9d0..deda2b821 100644 --- a/contrib/generated_schema.json +++ b/contrib/generated_schema.json @@ -323,6 +323,7 @@ "100base-tx", "100base-t1", "1000base-t", + "1000base-tx", "2.5gbase-t", "5gbase-t", "10gbase-t", diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 21ae20f05..2a8f252aa 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -65,12 +65,6 @@ class AnotherCustomScript(Script): script_order = (MyCustomScript, AnotherCustomScript) ``` -## Module Attributes - -### `name` - -You can define `name` within a script module (the Python file which contains one or more scripts) to set the module name. If `name` is not defined, the module's file name will be used. - ## Script Attributes Script attributes are defined under a class named `Meta` within the script. These are optional, but encouraged. diff --git a/docs/plugins/development/views.md b/docs/plugins/development/views.md index 3c13a6fcb..f6624f42c 100644 --- a/docs/plugins/development/views.md +++ b/docs/plugins/development/views.md @@ -195,12 +195,15 @@ Plugins can inject custom content into certain areas of core NetBox views. This | Method | View | Description | |---------------------|-------------|-----------------------------------------------------| +| `navbar()` | All | Inject content inside the top navigation bar | | `left_page()` | Object view | Inject content on the left side of the page | | `right_page()` | Object view | Inject content on the right side of the page | | `full_width_page()` | Object view | Inject content across the entire bottom of the page | | `buttons()` | Object view | Add buttons to the top of the page | | `list_buttons()` | List view | Add buttons to the top of the page | +!!! info "The `navbar()` method was introduced in NetBox v4.1." + Additionally, a `render()` method is available for convenience. This method accepts the name of a template to render, and any additional context data you want to pass. Its use is optional, however. When a PluginTemplateExtension is instantiated, context data is assigned to `self.context`. Available data include: diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index d837d0cf2..ae0578690 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -1,18 +1,34 @@ # NetBox v4.0 -## v4.0.4 (FUTURE) +## v4.0.6 (FUTURE) + +--- + +## v4.0.5 (2024-06-06) ### Enhancements * [#14810](https://github.com/netbox-community/netbox/issues/14810) - Enable contact assignment for services * [#15489](https://github.com/netbox-community/netbox/issues/15489) - Add 1000Base-TX interface type +* [#15873](https://github.com/netbox-community/netbox/issues/15873) - Improve readability of allocates resource numbers for clusters * [#16290](https://github.com/netbox-community/netbox/issues/16290) - Capture entire object in changelog data (but continue to display only non-internal attributes) +* [#16353](https://github.com/netbox-community/netbox/issues/16353) - Enable plugins to extend object change view with custom content ### Bug Fixes * [#13422](https://github.com/netbox-community/netbox/issues/13422) - Rebuild MPTT trees for applicable models after merging staged changes +* [#14567](https://github.com/netbox-community/netbox/issues/14567) - Apply active quicksearch value when exporting "current view" from object list +* [#15194](https://github.com/netbox-community/netbox/issues/15194) - Avoid enqueuing duplicate event triggers for a modified object +* [#16039](https://github.com/netbox-community/netbox/issues/16039) - Fix row highlighting for front & rear port connections under device view +* [#16050](https://github.com/netbox-community/netbox/issues/16050) - Fix display of names & descriptions defined for custom scripts +* [#16083](https://github.com/netbox-community/netbox/issues/16083) - Disable font ligatures to avoid peculiarities in rendered text * [#16202](https://github.com/netbox-community/netbox/issues/16202) - Fix site map button URL for certain localizations +* [#16261](https://github.com/netbox-community/netbox/issues/16261) - Fix GraphQL filtering for certain multi-value filters * [#16286](https://github.com/netbox-community/netbox/issues/16286) - Fix global search support for provider accounts +* [#16312](https://github.com/netbox-community/netbox/issues/16312) - Fix object list navigation for dashboard widgets +* [#16315](https://github.com/netbox-community/netbox/issues/16315) - Fix filtering change log & journal entries by object type in UI +* [#16376](https://github.com/netbox-community/netbox/issues/16376) - Update change log for the terminating object (e.g. interface) when attaching a cable +* [#16400](https://github.com/netbox-community/netbox/issues/16400) - Fix AttributeError when attempting to restore a previous configuration revision after deleting the current one --- diff --git a/netbox/core/management/commands/nbshell.py b/netbox/core/management/commands/nbshell.py index b96870252..7270c005a 100644 --- a/netbox/core/management/commands/nbshell.py +++ b/netbox/core/management/commands/nbshell.py @@ -18,7 +18,7 @@ BANNER_TEXT = """### NetBox interactive shell ({node}) node=platform.node(), python=platform.python_version(), django=get_version(), - netbox=settings.VERSION + netbox=settings.RELEASE.name ) diff --git a/netbox/core/views.py b/netbox/core/views.py index 5a65c5755..af705c8d1 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -224,7 +224,7 @@ class ConfigRevisionRestoreView(ContentTypePermissionRequiredMixin, View): for param in PARAMS: params.append(( param.name, - current_config.data.get(param.name, None), + current_config.data.get(param.name, None) if current_config else None, candidate_config.data.get(param.name, None) )) @@ -539,7 +539,7 @@ class SystemView(UserPassesTestMixin, View): except (ProgrammingError, IndexError): pass stats = { - 'netbox_version': settings.VERSION, + 'netbox_release': settings.RELEASE, 'django_version': DJANGO_VERSION, 'python_version': platform.python_version(), 'postgresql_version': psql_version, diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 2fb1e9949..a4d75654e 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -698,9 +698,6 @@ class DeviceTypeComponentFilterSet(django_filters.FilterSet): label=_('Device type (ID)'), ) - # TODO: Remove in v4.1 - devicetype_id = device_type_id - def search(self, queryset, name, value): if not value.strip(): return queryset @@ -717,9 +714,6 @@ class ModularDeviceTypeComponentFilterSet(DeviceTypeComponentFilterSet): label=_('Module type (ID)'), ) - # TODO: Remove in v4.1 - moduletype_id = module_type_id - class ConsolePortTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeComponentFilterSet): diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 64f0b8560..7afead829 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -355,11 +355,11 @@ class CableTermination(ChangeLoggedModel): super().save(*args, **kwargs) # Set the cable on the terminating object - termination_model = self.termination._meta.model - termination_model.objects.filter(pk=self.termination_id).update( - cable=self.cable, - cable_end=self.cable_end - ) + termination = self.termination._meta.model.objects.get(pk=self.termination_id) + termination.snapshot() + termination.cable = self.cable + termination.cable_end = self.cable_end + termination.save() def delete(self, *args, **kwargs): diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 0a22f5a82..df0dc7c7e 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -50,9 +50,9 @@ class DeviceComponentTemplateFilterSetTests: params = {'description': ['foobar1', 'foobar2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_devicetype_id(self): + def test_device_type_id(self): device_types = DeviceType.objects.all()[:2] - params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + params = {'device_type_id': [device_types[0].pk, device_types[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) @@ -1753,9 +1753,9 @@ class InventoryItemTemplateTestCase(TestCase, DeviceComponentTemplateFilterSetTe params = {'name': ['Inventory Item 1', 'Inventory Item 2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_devicetype_id(self): + def test_device_type_id(self): device_types = DeviceType.objects.all()[:2] - params = {'devicetype_id': [device_types[0].pk, device_types[1].pk]} + params = {'device_type_id': [device_types[0].pk, device_types[1].pk]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) def test_label(self): diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index add81a318..cc11664e6 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -329,7 +329,7 @@ class RSSFeedWidget(DashboardWidget): try: response = requests.get( url=self.config['feed_url'], - headers={'User-Agent': f'NetBox/{settings.VERSION}'}, + headers={'User-Agent': f'NetBox/{settings.RELEASE.version}'}, proxies=settings.HTTP_PROXIES, timeout=3 ) diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 274834d31..c3ac3e6ab 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -589,10 +589,6 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet): label=_('Data file (ID)'), ) - # TODO: Remove in v4.1 - role = device_role - role_id = device_role_id - class Meta: model = ConfigContext fields = ('id', 'name', 'is_active', 'description', 'weight', 'auto_sync_enabled', 'data_synced') diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index d58f5bfc9..5cdfac34e 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -912,10 +912,6 @@ class VLANGroupFilterSet(OrganizationalModelFilterSet): method='filter_scope' ) - # TODO: Remove in v4.1 - sitegroup = site_group - clustergroup = cluster_group - class Meta: model = VLANGroup fields = ('id', 'name', 'slug', 'min_vid', 'max_vid', 'description', 'scope_id') @@ -1106,10 +1102,6 @@ class ServiceFilterSet(NetBoxModelFilterSet): lookup_expr='contains' ) - # TODO: Remove in v4.1 - ipaddress = ip_address - ipaddress_id = ip_address_id - class Meta: model = Service fields = ('id', 'name', 'protocol', 'description') diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 3a46423a5..8f07a241a 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -1525,8 +1525,8 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'region': Region.objects.first().pk} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - def test_sitegroup(self): - params = {'sitegroup': SiteGroup.objects.first().pk} + def test_site_group(self): + params = {'site_group': SiteGroup.objects.first().pk} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_site(self): @@ -1541,8 +1541,8 @@ class VLANGroupTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'rack': Rack.objects.first().pk} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) - def test_clustergroup(self): - params = {'clustergroup': ClusterGroup.objects.first().pk} + def test_cluster_group(self): + params = {'cluster_group': ClusterGroup.objects.first().pk} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) def test_cluster(self): diff --git a/netbox/netbox/api/views.py b/netbox/netbox/api/views.py index cfbe82f14..d58d1affe 100644 --- a/netbox/netbox/api/views.py +++ b/netbox/netbox/api/views.py @@ -66,7 +66,7 @@ class StatusView(APIView): return Response({ 'django-version': DJANGO_VERSION, 'installed-apps': installed_apps, - 'netbox-version': settings.VERSION, + 'netbox-version': settings.RELEASE.full_version, 'plugins': get_installed_plugins(), 'python-version': platform.python_version(), 'rq-workers-running': Worker.count(get_connection('default')), diff --git a/netbox/netbox/plugins/registration.py b/netbox/netbox/plugins/registration.py index d27bb67ca..fbece12e5 100644 --- a/netbox/netbox/plugins/registration.py +++ b/netbox/netbox/plugins/registration.py @@ -32,21 +32,13 @@ def register_template_extensions(class_list): template_extension=template_extension ) ) - if template_extension.model is None: - raise TypeError( - _("PluginTemplateExtension class {template_extension} does not define a valid model!").format( - template_extension=template_extension - ) - ) registry['plugins']['template_extensions'][template_extension.model].append(template_extension) def register_menu(menu): if not isinstance(menu, PluginMenu): - raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format( - item=menu_link - )) + raise TypeError(_("{item} must be an instance of netbox.plugins.PluginMenuItem").format(item=menu)) registry['plugins']['menus'].append(menu) diff --git a/netbox/netbox/plugins/templates.py b/netbox/netbox/plugins/templates.py index 85229dbaf..ccd549160 100644 --- a/netbox/netbox/plugins/templates.py +++ b/netbox/netbox/plugins/templates.py @@ -14,7 +14,8 @@ class PluginTemplateExtension: The `model` attribute on the class defines the which model detail page this class renders content for. It should be set as a string in the form '.'. render() provides the following context data: - * object - The object being viewed + * object - The object being viewed (object views only) + * model - The type of object being viewed (list views only) * request - The current request * settings - Global NetBox settings * config - Plugin-specific configuration parameters @@ -36,6 +37,13 @@ class PluginTemplateExtension: return get_template(template_name).render({**self.context, **extra_context}) + def navbar(self): + """ + Content that will be rendered inside the top navigation menu. Content should be returned as an HTML + string. Note that content does not need to be marked as safe because this is automatically handled. + """ + raise NotImplementedError + def left_page(self): """ Content that will be rendered on the left of the detail page view. Content should be returned as an diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index b764fd930..40cfc3b39 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -18,6 +18,7 @@ from django.utils.translation import gettext_lazy as _ from netbox.config import PARAMS as CONFIG_PARAMS from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW from netbox.plugins import PluginConfig +from utilities.release import load_release_data from utilities.string import trailing_slash @@ -25,7 +26,8 @@ from utilities.string import trailing_slash # Environment setup # -VERSION = '4.0.4-dev' +RELEASE = load_release_data() +VERSION = RELEASE.full_version # Retained for backward compatibility HOSTNAME = platform.node() # Set the base directory two levels up BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -368,6 +370,8 @@ INSTALLED_APPS = [ 'drf_spectacular', 'drf_spectacular_sidecar', ] +if not DEBUG: + INSTALLED_APPS.remove('debug_toolbar') if not DJANGO_ADMIN_ENABLED: INSTALLED_APPS.remove('django.contrib.admin') @@ -531,7 +535,7 @@ if SENTRY_ENABLED: # Initialize the SDK sentry_sdk.init( dsn=SENTRY_DSN, - release=VERSION, + release=RELEASE.full_version, sample_rate=SENTRY_SAMPLE_RATE, traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE, send_default_pii=True, @@ -551,7 +555,7 @@ if SENTRY_ENABLED: DEPLOYMENT_ID = hashlib.sha256(SECRET_KEY.encode('utf-8')).hexdigest()[:16] CENSUS_URL = 'https://census.netbox.dev/api/v1/' CENSUS_PARAMS = { - 'version': VERSION, + 'version': RELEASE.full_version, 'python_version': sys.version.split()[0], 'deployment_id': DEPLOYMENT_ID, } @@ -609,7 +613,7 @@ FILTERS_NULL_CHOICE_VALUE = 'null' # Django REST framework (API) # -REST_FRAMEWORK_VERSION = '.'.join(VERSION.split('-')[0].split('.')[:2]) # Use major.minor as API version +REST_FRAMEWORK_VERSION = '.'.join(RELEASE.version.split('-')[0].split('.')[:2]) # Use major.minor as API version REST_FRAMEWORK = { 'ALLOWED_VERSIONS': [REST_FRAMEWORK_VERSION], 'COERCE_DECIMAL_TO_STRING': False, @@ -654,7 +658,7 @@ REST_FRAMEWORK = { SPECTACULAR_SETTINGS = { 'TITLE': 'NetBox REST API', 'LICENSE': {'name': 'Apache v2 License'}, - 'VERSION': VERSION, + 'VERSION': RELEASE.full_version, 'COMPONENT_SPLIT_REQUEST': True, 'REDOC_DIST': 'SIDECAR', 'SERVERS': [{ @@ -800,7 +804,7 @@ for plugin_name in PLUGINS: # Validate user-provided configuration settings and assign defaults if plugin_name not in PLUGINS_CONFIG: PLUGINS_CONFIG[plugin_name] = {} - plugin_config.validate(PLUGINS_CONFIG[plugin_name], VERSION) + plugin_config.validate(PLUGINS_CONFIG[plugin_name], RELEASE.version) # Add middleware plugin_middleware = plugin_config.middleware diff --git a/netbox/netbox/tests/dummy_plugin/template_content.py b/netbox/netbox/tests/dummy_plugin/template_content.py index b63338f2f..764faa60e 100644 --- a/netbox/netbox/tests/dummy_plugin/template_content.py +++ b/netbox/netbox/tests/dummy_plugin/template_content.py @@ -1,6 +1,12 @@ from netbox.plugins.templates import PluginTemplateExtension +class GlobalContent(PluginTemplateExtension): + + def navbar(self): + return "GLOBAL CONTENT - NAVBAR" + + class SiteContent(PluginTemplateExtension): model = 'dcim.site' @@ -20,4 +26,4 @@ class SiteContent(PluginTemplateExtension): return "SITE CONTENT - LIST BUTTONS" -template_extensions = [SiteContent] +template_extensions = [GlobalContent, SiteContent] diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py index 9ce20e204..351fef9e2 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -99,8 +99,9 @@ class PluginTest(TestCase): """ Check that plugin TemplateExtensions are registered. """ - from netbox.tests.dummy_plugin.template_content import SiteContent + from netbox.tests.dummy_plugin.template_content import GlobalContent, SiteContent + self.assertIn(GlobalContent, registry['plugins']['template_extensions'][None]) self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site']) def test_registered_columns(self): @@ -165,11 +166,11 @@ class PluginTest(TestCase): required_settings = ['foo'] # Validation should pass when all required settings are present - DummyConfigWithRequiredSettings.validate({'foo': True}, settings.VERSION) + DummyConfigWithRequiredSettings.validate({'foo': True}, settings.RELEASE.version) # Validation should fail when a required setting is missing with self.assertRaises(ImproperlyConfigured): - DummyConfigWithRequiredSettings.validate({}, settings.VERSION) + DummyConfigWithRequiredSettings.validate({}, settings.RELEASE.version) def test_default_settings(self): """ @@ -182,12 +183,12 @@ class PluginTest(TestCase): # Populate the default value if setting has not been specified user_config = {} - DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION) + DummyConfigWithDefaultSettings.validate(user_config, settings.RELEASE.version) self.assertEqual(user_config['bar'], 123) # Don't overwrite specified values user_config = {'bar': 456} - DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION) + DummyConfigWithDefaultSettings.validate(user_config, settings.RELEASE.version) self.assertEqual(user_config['bar'], 456) def test_graphql(self): diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index a5e5e36f4..b0175ec04 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -57,7 +57,7 @@ _patterns = [ path( "api/schema/", - cache_page(timeout=86400, key_prefix=f"api_schema_{settings.VERSION}")( + cache_page(timeout=86400, key_prefix=f"api_schema_{settings.RELEASE.version}")( SpectacularAPIView.as_view() ), name="schema", diff --git a/netbox/netbox/views/errors.py b/netbox/netbox/views/errors.py index a0f783ed6..9e8ed5a3a 100644 --- a/netbox/netbox/views/errors.py +++ b/netbox/netbox/views/errors.py @@ -54,7 +54,7 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME): return HttpResponseServerError(template.render({ 'error': error, 'exception': str(type_), - 'netbox_version': settings.VERSION, + 'netbox_version': settings.RELEASE.full_version, 'python_version': platform.python_version(), 'plugins': get_installed_plugins(), })) diff --git a/netbox/netbox/views/misc.py b/netbox/netbox/views/misc.py index 9678b71e3..569fcf728 100644 --- a/netbox/netbox/views/misc.py +++ b/netbox/netbox/views/misc.py @@ -50,7 +50,7 @@ class HomeView(View): latest_release = cache.get('latest_release') if latest_release: release_version, release_url = latest_release - if release_version > version.parse(settings.VERSION): + if release_version > version.parse(settings.RELEASE.version): new_release = { 'version': str(release_version), 'url': release_url, diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index 0ec53a6d2..0696d2e82 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 58c419b3d..59f703d4b 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index f70987c66..00b92809a 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/package.json b/netbox/project-static/package.json index e69037f9d..da8b7d229 100644 --- a/netbox/project-static/package.json +++ b/netbox/project-static/package.json @@ -27,10 +27,10 @@ "bootstrap": "5.3.3", "clipboard": "2.0.11", "flatpickr": "4.6.13", - "gridstack": "10.1.2", + "gridstack": "10.2.0", "htmx.org": "1.9.12", "query-string": "9.0.0", - "sass": "1.77.2", + "sass": "1.77.4", "tom-select": "2.3.1", "typeface-inter": "3.18.1", "typeface-roboto-mono": "1.1.13" diff --git a/netbox/project-static/src/search.ts b/netbox/project-static/src/search.ts index 4be740196..1295527cf 100644 --- a/netbox/project-static/src/search.ts +++ b/netbox/project-static/src/search.ts @@ -7,38 +7,74 @@ import { isTruthy } from './util'; */ function quickSearchEventHandler(event: Event): void { const quicksearch = event.currentTarget as HTMLInputElement; - const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement; + const clearbtn = document.getElementById('quicksearch_clear') as HTMLAnchorElement; if (isTruthy(clearbtn)) { - if (quicksearch.value === "") { - clearbtn.classList.add("invisible"); + if (quicksearch.value === '') { + clearbtn.classList.add('invisible'); } else { - clearbtn.classList.remove("invisible"); + clearbtn.classList.remove('invisible'); } } } +/** + * Clear the existing search parameters in the link to export Current View. + */ +function clearLinkParams(): void { + const link = document.getElementById('export_current_view') as HTMLLinkElement; + const linkUpdated = link?.href.split('&')[0]; + link.setAttribute('href', linkUpdated); +} + +/** + * Update the Export View link to add the Quick Search parameters. + * @param event + */ +function handleQuickSearchParams(event: Event): void { + const quickSearchParameters = event.currentTarget as HTMLInputElement; + + // Clear the existing search parameters + clearLinkParams(); + + if (quickSearchParameters != null) { + const link = document.getElementById('export_current_view') as HTMLLinkElement; + const search_parameter = `q=${quickSearchParameters.value}`; + const linkUpdated = link?.href + '&' + search_parameter; + link.setAttribute('href', linkUpdated); + } +} + /** * Initialize Quicksearch Event listener/handlers. */ export function initQuickSearch(): void { - const quicksearch = document.getElementById("quicksearch") as HTMLInputElement; - const clearbtn = document.getElementById("quicksearch_clear") as HTMLAnchorElement; + const quicksearch = document.getElementById('quicksearch') as HTMLInputElement; + const clearbtn = document.getElementById('quicksearch_clear') as HTMLAnchorElement; if (isTruthy(quicksearch)) { - quicksearch.addEventListener("keyup", quickSearchEventHandler, { - passive: true - }) - quicksearch.addEventListener("search", quickSearchEventHandler, { - passive: true - }) + quicksearch.addEventListener('keyup', quickSearchEventHandler, { + passive: true, + }); + quicksearch.addEventListener('search', quickSearchEventHandler, { + passive: true, + }); + quicksearch.addEventListener('change', handleQuickSearchParams, { + passive: true, + }); + if (isTruthy(clearbtn)) { - clearbtn.addEventListener("click", async () => { - const search = new Event('search'); - quicksearch.value = ''; - await new Promise(f => setTimeout(f, 100)); - quicksearch.dispatchEvent(search); - }, { - passive: true - }) + clearbtn.addEventListener( + 'click', + async () => { + const search = new Event('search'); + quicksearch.value = ''; + await new Promise(f => setTimeout(f, 100)); + quicksearch.dispatchEvent(search); + clearLinkParams(); + }, + { + passive: true, + }, + ); } } } diff --git a/netbox/project-static/styles/overrides/_tabler.scss b/netbox/project-static/styles/overrides/_tabler.scss index 5ea63be76..97f1298df 100644 --- a/netbox/project-static/styles/overrides/_tabler.scss +++ b/netbox/project-static/styles/overrides/_tabler.scss @@ -39,3 +39,8 @@ table a { // Adjust table anchor link contrast as not enough contrast in dark mode filter: brightness(110%); } + +// Override background color alpha value +[data-bs-theme=dark] ::selection { + background-color: rgba(var(--tblr-primary-rgb),.48) +} diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock index 16c8b4dbc..7ce1162a5 100644 --- a/netbox/project-static/yarn.lock +++ b/netbox/project-static/yarn.lock @@ -1754,10 +1754,10 @@ graphql@16.8.1: resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== -gridstack@10.1.2: - version "10.1.2" - resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.1.2.tgz#58b5ae0057a8aa5e4f6563041c4ca2def3aa4268" - integrity sha512-Nn27XGQ68WtBC513cKQQ4t/dA2uuN/xnNUU50puXEJv6IFk5SzT0Dnsq68GpopO1n0tXUKZKm1Rw7uOUMDz1KQ== +gridstack@10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.2.0.tgz#4ba9c7ee69a730851721a9f5cb33dc55026ded1f" + integrity sha512-svKAOq/dfinpvhe/nnxdyZOOEd9qynXiOPHvL96PALE0yWChWp/6lechnqKwud0tL/rRyAfMJ6Hh/z2fS13pBA== has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" @@ -2482,10 +2482,10 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -sass@1.77.2: - version "1.77.2" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.2.tgz#18d4ed2eefc260cdc8099c5439ec1303fd5863aa" - integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA== +sass@1.77.4: + version "1.77.4" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.4.tgz#92059c7bfc56b827c56eb116778d157ec017a5cd" + integrity sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" diff --git a/netbox/release.yaml b/netbox/release.yaml new file mode 100644 index 000000000..83bddf615 --- /dev/null +++ b/netbox/release.yaml @@ -0,0 +1,2 @@ +version: "4.1.0" +designation: "dev" diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index 7a7a4fe99..1030c469b 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -20,7 +20,7 @@ {# Initialize color mode #} {% django_htmx_script %} diff --git a/netbox/templates/base/layout.html b/netbox/templates/base/layout.html index d53591cb4..397553446 100644 --- a/netbox/templates/base/layout.html +++ b/netbox/templates/base/layout.html @@ -2,6 +2,7 @@ {% extends 'base/base.html' %} {% load helpers %} {% load navigation %} +{% load plugins %} {% load static %} {% load i18n %} @@ -51,8 +52,12 @@ Blocks: