mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06:00
Merge branch 'develop' into 17292-detect-infinite-loop-in-cable-trace
This commit is contained in:
commit
0d75210649
@ -14,7 +14,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.1.7
|
placeholder: v4.1.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/02-bug_report.yaml
vendored
@ -39,7 +39,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: NetBox Version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v4.1.7
|
placeholder: v4.1.8
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
@ -49,6 +49,10 @@ This key lists all models which have been registered in NetBox which are not des
|
|||||||
|
|
||||||
This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.
|
This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.
|
||||||
|
|
||||||
|
### `request_processors`
|
||||||
|
|
||||||
|
A list of context managers to invoke when processing a request e.g. in middleware or when executing a background job. Request processors can be registered with the `@register_request_processor` decorator.
|
||||||
|
|
||||||
### `search`
|
### `search`
|
||||||
|
|
||||||
A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
|
A dictionary mapping each model (identified by its app and label) to its search index class, if one has been registered for it.
|
||||||
|
@ -1,6 +1,32 @@
|
|||||||
# NetBox v4.1
|
# NetBox v4.1
|
||||||
|
|
||||||
## v4.1.7 (FUTURE)
|
## v4.1.8 (2024-12-12)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#17071](https://github.com/netbox-community/netbox/issues/17071) - Enable OOB IP address designation during bulk import
|
||||||
|
* [#17465](https://github.com/netbox-community/netbox/issues/17465) - Enable designation of rack type during bulk import & bulk edit
|
||||||
|
* [#17889](https://github.com/netbox-community/netbox/issues/17889) - Enable designating an IP address as out-of-band for a device upon creation
|
||||||
|
* [#17960](https://github.com/netbox-community/netbox/issues/17960) - Add L2TP, PPTP, Wireguard, and OpenVPN tunnel types
|
||||||
|
* [#18021](https://github.com/netbox-community/netbox/issues/18021) - Automatically clear cache on restart when `DEBUG` is enabled
|
||||||
|
* [#18061](https://github.com/netbox-community/netbox/issues/18061) - Omit stack trace from rendered device/VM configuration when an exception is raised
|
||||||
|
* [#18065](https://github.com/netbox-community/netbox/issues/18065) - Include status in device details when hovering on rack elevation
|
||||||
|
* [#18211](https://github.com/netbox-community/netbox/issues/18211) - Enable the dynamic registration of context managers for request processing
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#14044](https://github.com/netbox-community/netbox/issues/14044) - Fix unhandled AttributeError exception when bulk renaming objects
|
||||||
|
* [#17490](https://github.com/netbox-community/netbox/issues/17490) - Fix dynamic inclusion support for config templates
|
||||||
|
* [#17810](https://github.com/netbox-community/netbox/issues/17810) - Fix validation of racked device fields when modifying via REST API
|
||||||
|
* [#17820](https://github.com/netbox-community/netbox/issues/17820) - Ensure default custom field values are populated when creating new modules
|
||||||
|
* [#18044](https://github.com/netbox-community/netbox/issues/18044) - Show plugin-generated alerts within UI views for custom scripts
|
||||||
|
* [#18150](https://github.com/netbox-community/netbox/issues/18150) - Fix REST API pagination for low `MAX_PAGE_SIZE` values
|
||||||
|
* [#18183](https://github.com/netbox-community/netbox/issues/18183) - Omit UI navigation bar when printing
|
||||||
|
* [#18213](https://github.com/netbox-community/netbox/issues/18213) - Fix searching for ASN ranges by name
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v4.1.7 (2024-11-21)
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.migrations.operations import AlterModelOptions
|
from django.db.migrations.operations import AlterModelOptions
|
||||||
|
|
||||||
@ -22,3 +24,7 @@ class CoreConfig(AppConfig):
|
|||||||
|
|
||||||
# Register models
|
# Register models
|
||||||
register_models(*self.get_models())
|
register_models(*self.get_models())
|
||||||
|
|
||||||
|
# Clear Redis cache on startup in development mode
|
||||||
|
if settings.DEBUG:
|
||||||
|
cache.clear()
|
||||||
|
@ -359,6 +359,11 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
queryset=RackRole.objects.all(),
|
queryset=RackRole.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
rack_type = DynamicModelChoiceField(
|
||||||
|
label=_('Rack type'),
|
||||||
|
queryset=RackType.objects.all(),
|
||||||
|
required=False,
|
||||||
|
)
|
||||||
serial = forms.CharField(
|
serial = forms.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
required=False,
|
required=False,
|
||||||
@ -438,7 +443,7 @@ class RackBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
|
|
||||||
model = Rack
|
model = Rack
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'description', name=_('Rack')),
|
FieldSet('status', 'role', 'tenant', 'serial', 'asset_tag', 'rack_type', 'description', name=_('Rack')),
|
||||||
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
FieldSet('region', 'site_group', 'site', 'location', name=_('Location')),
|
||||||
FieldSet(
|
FieldSet(
|
||||||
'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit',
|
'form_factor', 'width', 'u_height', 'desc_units', 'airflow', 'outer_width', 'outer_depth', 'outer_unit',
|
||||||
|
@ -256,6 +256,13 @@ class RackImportForm(NetBoxModelImportForm):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Name of assigned role')
|
help_text=_('Name of assigned role')
|
||||||
)
|
)
|
||||||
|
rack_type = CSVModelChoiceField(
|
||||||
|
label=_('Rack type'),
|
||||||
|
queryset=RackType.objects.all(),
|
||||||
|
to_field_name='model',
|
||||||
|
required=False,
|
||||||
|
help_text=_('Rack type model')
|
||||||
|
)
|
||||||
form_factor = CSVChoiceField(
|
form_factor = CSVChoiceField(
|
||||||
label=_('Type'),
|
label=_('Type'),
|
||||||
choices=RackFormFactorChoices,
|
choices=RackFormFactorChoices,
|
||||||
@ -265,8 +272,13 @@ class RackImportForm(NetBoxModelImportForm):
|
|||||||
width = forms.ChoiceField(
|
width = forms.ChoiceField(
|
||||||
label=_('Width'),
|
label=_('Width'),
|
||||||
choices=RackWidthChoices,
|
choices=RackWidthChoices,
|
||||||
|
required=False,
|
||||||
help_text=_('Rail-to-rail width (in inches)')
|
help_text=_('Rail-to-rail width (in inches)')
|
||||||
)
|
)
|
||||||
|
u_height = forms.IntegerField(
|
||||||
|
required=False,
|
||||||
|
label=_('Height (U)')
|
||||||
|
)
|
||||||
outer_unit = CSVChoiceField(
|
outer_unit = CSVChoiceField(
|
||||||
label=_('Outer unit'),
|
label=_('Outer unit'),
|
||||||
choices=RackDimensionUnitChoices,
|
choices=RackDimensionUnitChoices,
|
||||||
@ -289,9 +301,9 @@ class RackImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Rack
|
model = Rack
|
||||||
fields = (
|
fields = (
|
||||||
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'form_factor', 'serial', 'asset_tag',
|
'site', 'location', 'name', 'facility_id', 'tenant', 'status', 'role', 'rack_type', 'form_factor', 'serial',
|
||||||
'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'airflow',
|
'asset_tag', 'width', 'u_height', 'desc_units', 'outer_width', 'outer_depth', 'outer_unit',
|
||||||
'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
'mounting_depth', 'airflow', 'weight', 'max_weight', 'weight_unit', 'description', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
@ -303,6 +315,16 @@ class RackImportForm(NetBoxModelImportForm):
|
|||||||
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
params = {f"site__{self.fields['site'].to_field_name}": data.get('site')}
|
||||||
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
self.fields['location'].queryset = self.fields['location'].queryset.filter(**params)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# width & u_height must be set if not specifying a rack type on import
|
||||||
|
if not self.instance.pk:
|
||||||
|
if not self.cleaned_data.get('rack_type') and not self.cleaned_data.get('width'):
|
||||||
|
raise forms.ValidationError(_("Width must be set if not specifying a rack type."))
|
||||||
|
if not self.cleaned_data.get('rack_type') and not self.cleaned_data.get('u_height'):
|
||||||
|
raise forms.ValidationError(_("U height must be set if not specifying a rack type."))
|
||||||
|
|
||||||
|
|
||||||
class RackReservationImportForm(NetBoxModelImportForm):
|
class RackReservationImportForm(NetBoxModelImportForm):
|
||||||
site = CSVModelChoiceField(
|
site = CSVModelChoiceField(
|
||||||
|
@ -1277,6 +1277,11 @@ class Module(PrimaryModel, ConfigContextModel):
|
|||||||
if not disable_replication:
|
if not disable_replication:
|
||||||
create_instances.append(template_instance)
|
create_instances.append(template_instance)
|
||||||
|
|
||||||
|
# Set default values for any applicable custom fields
|
||||||
|
if cf_defaults := CustomField.objects.get_defaults_for_model(component_model):
|
||||||
|
for component in create_instances:
|
||||||
|
component.custom_field_data = cf_defaults
|
||||||
|
|
||||||
if component_model is not ModuleBay:
|
if component_model is not ModuleBay:
|
||||||
component_model.objects.bulk_create(create_instances)
|
component_model.objects.bulk_create(create_instances)
|
||||||
# Emit the post_save signal for each newly created object
|
# Emit the post_save signal for each newly created object
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import traceback
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.paginator import EmptyPage, PageNotAnInteger
|
from django.core.paginator import EmptyPage, PageNotAnInteger
|
||||||
@ -2106,7 +2104,8 @@ class DeviceRenderConfigView(generic.ObjectView):
|
|||||||
# If a direct export has been requested, return the rendered template content as a
|
# If a direct export has been requested, return the rendered template content as a
|
||||||
# downloadable file.
|
# downloadable file.
|
||||||
if request.GET.get('export'):
|
if request.GET.get('export'):
|
||||||
response = HttpResponse(context['rendered_config'], content_type='text')
|
content = context['rendered_config'] or context['error_message']
|
||||||
|
response = HttpResponse(content, content_type='text')
|
||||||
filename = f"{instance.name or 'config'}.txt"
|
filename = f"{instance.name or 'config'}.txt"
|
||||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||||
return response
|
return response
|
||||||
@ -2124,17 +2123,18 @@ class DeviceRenderConfigView(generic.ObjectView):
|
|||||||
|
|
||||||
# Render the config template
|
# Render the config template
|
||||||
rendered_config = None
|
rendered_config = None
|
||||||
|
error_message = None
|
||||||
if config_template := instance.get_config_template():
|
if config_template := instance.get_config_template():
|
||||||
try:
|
try:
|
||||||
rendered_config = config_template.render(context=context_data)
|
rendered_config = config_template.render(context=context_data)
|
||||||
except TemplateError as e:
|
except TemplateError as e:
|
||||||
messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
|
error_message = _("An error occurred while rendering the template: {error}").format(error=e)
|
||||||
rendered_config = traceback.format_exc()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'config_template': config_template,
|
'config_template': config_template,
|
||||||
'context_data': context_data,
|
'context_data': context_data,
|
||||||
'rendered_config': rendered_config,
|
'rendered_config': rendered_config,
|
||||||
|
'error_message': error_message,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -211,8 +211,10 @@ class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
|||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
qs_filter = Q(description__icontains=value)
|
return queryset.filter(
|
||||||
return queryset.filter(qs_filter)
|
Q(name__icontains=value) |
|
||||||
|
Q(description__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
|
||||||
|
@ -38,12 +38,14 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
|
|||||||
|
|
||||||
def get_limit(self, request):
|
def get_limit(self, request):
|
||||||
if self.limit_query_param:
|
if self.limit_query_param:
|
||||||
|
MAX_PAGE_SIZE = get_config().MAX_PAGE_SIZE
|
||||||
|
if MAX_PAGE_SIZE:
|
||||||
|
MAX_PAGE_SIZE = max(MAX_PAGE_SIZE, self.default_limit)
|
||||||
try:
|
try:
|
||||||
limit = int(request.query_params[self.limit_query_param])
|
limit = int(request.query_params[self.limit_query_param])
|
||||||
if limit < 0:
|
if limit < 0:
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
# Enforce maximum page size, if defined
|
# Enforce maximum page size, if defined
|
||||||
MAX_PAGE_SIZE = get_config().MAX_PAGE_SIZE
|
|
||||||
if MAX_PAGE_SIZE:
|
if MAX_PAGE_SIZE:
|
||||||
return MAX_PAGE_SIZE if limit == 0 else min(limit, MAX_PAGE_SIZE)
|
return MAX_PAGE_SIZE if limit == 0 else min(limit, MAX_PAGE_SIZE)
|
||||||
return limit
|
return limit
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from netbox.context import current_request, events_queue
|
from netbox.context import current_request, events_queue
|
||||||
|
from netbox.utils import register_request_processor
|
||||||
from extras.events import flush_events
|
from extras.events import flush_events
|
||||||
|
|
||||||
|
|
||||||
|
@register_request_processor
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def event_tracking(request):
|
def event_tracking(request):
|
||||||
"""
|
"""
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from contextlib import ExitStack
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@ -10,7 +12,7 @@ from django.db.utils import InternalError
|
|||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
|
|
||||||
from netbox.config import clear_config, get_config
|
from netbox.config import clear_config, get_config
|
||||||
from netbox.context_managers import event_tracking
|
from netbox.registry import registry
|
||||||
from netbox.views import handler_500
|
from netbox.views import handler_500
|
||||||
from utilities.api import is_api_request
|
from utilities.api import is_api_request
|
||||||
from utilities.error_handlers import handle_rest_api_exception
|
from utilities.error_handlers import handle_rest_api_exception
|
||||||
@ -32,8 +34,10 @@ class CoreMiddleware:
|
|||||||
# Assign a random unique ID to the request. This will be used for change logging.
|
# Assign a random unique ID to the request. This will be used for change logging.
|
||||||
request.id = uuid.uuid4()
|
request.id = uuid.uuid4()
|
||||||
|
|
||||||
# Enable the event_tracking context manager and process the request.
|
# Apply all registered request processors
|
||||||
with event_tracking(request):
|
with ExitStack() as stack:
|
||||||
|
for request_processor in registry['request_processors']:
|
||||||
|
stack.enter_context(request_processor(request))
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
|
|
||||||
# Check if language cookie should be renewed
|
# Check if language cookie should be renewed
|
||||||
|
@ -29,6 +29,7 @@ registry = Registry({
|
|||||||
'model_features': dict(),
|
'model_features': dict(),
|
||||||
'models': collections.defaultdict(set),
|
'models': collections.defaultdict(set),
|
||||||
'plugins': dict(),
|
'plugins': dict(),
|
||||||
|
'request_processors': list(),
|
||||||
'search': dict(),
|
'search': dict(),
|
||||||
'tables': collections.defaultdict(dict),
|
'tables': collections.defaultdict(dict),
|
||||||
'views': collections.defaultdict(dict),
|
'views': collections.defaultdict(dict),
|
||||||
|
@ -3,6 +3,7 @@ from netbox.registry import registry
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'get_data_backend_choices',
|
'get_data_backend_choices',
|
||||||
'register_data_backend',
|
'register_data_backend',
|
||||||
|
'register_request_processor',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -24,3 +25,12 @@ def register_data_backend():
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def register_request_processor(func):
|
||||||
|
"""
|
||||||
|
Decorator for registering a request processor.
|
||||||
|
"""
|
||||||
|
registry['request_processors'].append(func)
|
||||||
|
|
||||||
|
return func
|
||||||
|
5568
netbox/project-static/dist/graphiql/graphiql.min.js
vendored
5568
netbox/project-static/dist/graphiql/graphiql.min.js
vendored
File diff suppressed because it is too large
Load Diff
BIN
netbox/project-static/dist/netbox-external.css
vendored
BIN
netbox/project-static/dist/netbox-external.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -6,8 +6,8 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphiql/plugin-explorer": "3.2.2",
|
"@graphiql/plugin-explorer": "3.2.3",
|
||||||
"graphiql": "3.7.1",
|
"graphiql": "3.7.2",
|
||||||
"graphql": "16.9.0",
|
"graphql": "16.9.0",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "netbox",
|
"name": "netbox",
|
||||||
"version": "4.0.0",
|
"version": "4.1.0",
|
||||||
"main": "dist/netbox.js",
|
"main": "dist/netbox.js",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
@ -27,11 +27,11 @@
|
|||||||
"bootstrap": "5.3.3",
|
"bootstrap": "5.3.3",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"flatpickr": "4.6.13",
|
"flatpickr": "4.6.13",
|
||||||
"gridstack": "10.3.1",
|
"gridstack": "11.1.2",
|
||||||
"htmx.org": "1.9.12",
|
"htmx.org": "1.9.12",
|
||||||
"query-string": "9.1.1",
|
"query-string": "9.1.1",
|
||||||
"sass": "1.80.5",
|
"sass": "1.82.0",
|
||||||
"tom-select": "2.3.1",
|
"tom-select": "2.4.1",
|
||||||
"typeface-inter": "3.18.1",
|
"typeface-inter": "3.18.1",
|
||||||
"typeface-roboto-mono": "1.1.13"
|
"typeface-roboto-mono": "1.1.13"
|
||||||
},
|
},
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import { RecursivePartial, TomInput, TomOption, TomSettings } from 'tom-select/dist/types/types';
|
import { RecursivePartial, TomOption, TomSettings } from 'tom-select/dist/types/types';
|
||||||
import { addClasses } from 'tom-select/src/vanilla'
|
import { TomInput } from 'tom-select/dist/cjs/types/core';
|
||||||
|
import { addClasses } from 'tom-select/src/vanilla.ts';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import TomSelect from 'tom-select';
|
import TomSelect from 'tom-select';
|
||||||
import type { Stringifiable } from 'query-string';
|
import type { Stringifiable } from 'query-string';
|
||||||
import { DynamicParamsMap } from './dynamicParamsMap';
|
import { DynamicParamsMap } from './dynamicParamsMap';
|
||||||
|
|
||||||
// Transitional
|
// Transitional
|
||||||
import { QueryFilter, PathFilter } from '../types'
|
import { QueryFilter, PathFilter } from '../types';
|
||||||
import { getElement, replaceAll } from '../../util';
|
import { getElement, replaceAll } from '../../util';
|
||||||
|
|
||||||
|
|
||||||
// Extends TomSelect to provide enhanced fetching of options via the REST API
|
// Extends TomSelect to provide enhanced fetching of options via the REST API
|
||||||
export class DynamicTomSelect extends TomSelect {
|
export class DynamicTomSelect extends TomSelect {
|
||||||
|
|
||||||
public readonly nullOption: Nullable<TomOption> = null;
|
public readonly nullOption: Nullable<TomOption> = null;
|
||||||
|
|
||||||
// Transitional code from APISelect
|
// Transitional code from APISelect
|
||||||
@ -25,7 +24,7 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
* Overrides
|
* Overrides
|
||||||
*/
|
*/
|
||||||
|
|
||||||
constructor( input_arg: string|TomInput, user_settings: RecursivePartial<TomSettings> ) {
|
constructor(input_arg: string | TomInput, user_settings: RecursivePartial<TomSettings>) {
|
||||||
super(input_arg, user_settings);
|
super(input_arg, user_settings);
|
||||||
|
|
||||||
// Glean the REST API endpoint URL from the <select> element
|
// Glean the REST API endpoint URL from the <select> element
|
||||||
@ -34,7 +33,8 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
// Override any field names set as widget attributes
|
// Override any field names set as widget attributes
|
||||||
this.valueField = this.input.getAttribute('ts-value-field') || this.settings.valueField;
|
this.valueField = this.input.getAttribute('ts-value-field') || this.settings.valueField;
|
||||||
this.labelField = this.input.getAttribute('ts-label-field') || this.settings.labelField;
|
this.labelField = this.input.getAttribute('ts-label-field') || this.settings.labelField;
|
||||||
this.disabledField = this.input.getAttribute('ts-disabled-field') || this.settings.disabledField;
|
this.disabledField =
|
||||||
|
this.input.getAttribute('ts-disabled-field') || this.settings.disabledField;
|
||||||
this.descriptionField = this.input.getAttribute('ts-description-field') || 'description';
|
this.descriptionField = this.input.getAttribute('ts-description-field') || 'description';
|
||||||
this.depthField = this.input.getAttribute('ts-depth-field') || '_depth';
|
this.depthField = this.input.getAttribute('ts-depth-field') || '_depth';
|
||||||
this.parentField = this.input.getAttribute('ts-parent-field') || null;
|
this.parentField = this.input.getAttribute('ts-parent-field') || null;
|
||||||
@ -43,9 +43,9 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
// Set the null option (if any)
|
// Set the null option (if any)
|
||||||
const nullOption = this.input.getAttribute('data-null-option');
|
const nullOption = this.input.getAttribute('data-null-option');
|
||||||
if (nullOption) {
|
if (nullOption) {
|
||||||
let valueField = this.settings.valueField;
|
const valueField = this.settings.valueField;
|
||||||
let labelField = this.settings.labelField;
|
const labelField = this.settings.labelField;
|
||||||
this.nullOption = {}
|
this.nullOption = {};
|
||||||
this.nullOption[valueField] = 'null';
|
this.nullOption[valueField] = 'null';
|
||||||
this.nullOption[labelField] = nullOption;
|
this.nullOption[labelField] = nullOption;
|
||||||
}
|
}
|
||||||
@ -98,8 +98,8 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(apiData => {
|
.then(apiData => {
|
||||||
const results: Dict[] = apiData.results;
|
const results: Dict[] = apiData.results;
|
||||||
let options: Dict[] = []
|
const options: Dict[] = [];
|
||||||
for (let result of results) {
|
for (const result of results) {
|
||||||
const option = self.getOptionFromData(result);
|
const option = self.getOptionFromData(result);
|
||||||
options.push(option);
|
options.push(option);
|
||||||
}
|
}
|
||||||
@ -108,10 +108,10 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
// Pass the options to the callback function
|
// Pass the options to the callback function
|
||||||
.then(options => {
|
.then(options => {
|
||||||
self.loadCallback(options, []);
|
self.loadCallback(options, []);
|
||||||
}).catch(()=>{
|
})
|
||||||
|
.catch(() => {
|
||||||
self.loadCallback([], []);
|
self.loadCallback([], []);
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -155,14 +155,14 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
|
|
||||||
// Compile TomOption data from an API result
|
// Compile TomOption data from an API result
|
||||||
getOptionFromData(data: Dict) {
|
getOptionFromData(data: Dict) {
|
||||||
let option: Dict = {
|
const option: Dict = {
|
||||||
id: data[this.valueField],
|
id: data[this.valueField],
|
||||||
display: data[this.labelField],
|
display: data[this.labelField],
|
||||||
depth: data[this.depthField] || null,
|
depth: data[this.depthField] || null,
|
||||||
description: data[this.descriptionField] || null,
|
description: data[this.descriptionField] || null,
|
||||||
};
|
};
|
||||||
if (data[this.parentField]) {
|
if (data[this.parentField]) {
|
||||||
let parent: Dict = data[this.parentField] as Dict;
|
const parent: Dict = data[this.parentField] as Dict;
|
||||||
option['parent'] = parent[this.labelField];
|
option['parent'] = parent[this.labelField];
|
||||||
}
|
}
|
||||||
if (data[this.countField]) {
|
if (data[this.countField]) {
|
||||||
@ -171,7 +171,7 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
if (data[this.disabledField]) {
|
if (data[this.disabledField]) {
|
||||||
option['disabled'] = data[this.disabledField];
|
option['disabled'] = data[this.disabledField];
|
||||||
}
|
}
|
||||||
return option
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -218,7 +218,6 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Parse the `data-url` attribute to add any variables to `pathValues` as keys with empty
|
// Parse the `data-url` attribute to add any variables to `pathValues` as keys with empty
|
||||||
// values. As those keys' corresponding form fields' values change, `pathValues` will be
|
// values. As those keys' corresponding form fields' values change, `pathValues` will be
|
||||||
// updated to reflect the new value.
|
// updated to reflect the new value.
|
||||||
@ -297,7 +296,8 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
// value. For example, if the dependency is the `rack` field, and the `rack` field's value
|
// value. For example, if the dependency is the `rack` field, and the `rack` field's value
|
||||||
// is `1`, this element's URL would change from `/dcim/racks/{{rack}}/` to `/dcim/racks/1/`.
|
// is `1`, this element's URL would change from `/dcim/racks/{{rack}}/` to `/dcim/racks/1/`.
|
||||||
const hasReplacement =
|
const hasReplacement =
|
||||||
this.api_url.includes(`{{`) && Boolean(this.api_url.match(new RegExp(`({{(${id})}})`, 'g')));
|
this.api_url.includes(`{{`) &&
|
||||||
|
Boolean(this.api_url.match(new RegExp(`({{(${id})}})`, 'g')));
|
||||||
|
|
||||||
if (hasReplacement) {
|
if (hasReplacement) {
|
||||||
if (element.value) {
|
if (element.value) {
|
||||||
@ -349,5 +349,4 @@ export class DynamicTomSelect extends TomSelect {
|
|||||||
// Load new data.
|
// Load new data.
|
||||||
this.load(this.lastValue);
|
this.load(this.lastValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
// Needed for tom-select/src/vanilla.ts
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
// tom-select v2.3.1 raises several TS6133 errors with noUnusedParameters
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
|
@ -200,17 +200,17 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5"
|
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5"
|
||||||
integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==
|
integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==
|
||||||
|
|
||||||
"@graphiql/plugin-explorer@3.2.2":
|
"@graphiql/plugin-explorer@3.2.3":
|
||||||
version "3.2.2"
|
version "3.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/@graphiql/plugin-explorer/-/plugin-explorer-3.2.2.tgz#973d6015b6db15041902e95c3e4b746473313eb6"
|
resolved "https://registry.yarnpkg.com/@graphiql/plugin-explorer/-/plugin-explorer-3.2.3.tgz#03854d7e62d6e24c6552ae6706e3945b9324fa23"
|
||||||
integrity sha512-zeBZJUAX9h+3nXw3GLHZoxi6wwYqDBU2L/xeSXSTagJhcLNW1Hwb/t/wb296hQ1x/9nyGySsTA0DQiiWV3rCBQ==
|
integrity sha512-yh5WXRqDPuKjVyNxUwXYjx8tImvVOx+2FGanLyjoAJP2LKQu6eDtButyJ8sExk1qW4+HCSrXxJNSPs4W7cYT3g==
|
||||||
dependencies:
|
dependencies:
|
||||||
graphiql-explorer "^0.9.0"
|
graphiql-explorer "^0.9.0"
|
||||||
|
|
||||||
"@graphiql/react@^0.26.2":
|
"@graphiql/react@^0.27.0":
|
||||||
version "0.26.2"
|
version "0.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.26.2.tgz#3a1a01a569b624de8141c53eed24a7db9a523668"
|
resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.27.0.tgz#4475a0f4ddf25d8ebc1bfc538fb21f5f1d435916"
|
||||||
integrity sha512-aO4GWf/kJmqrjO+PORT/NPxwGvPGlg+mwye1v8xAlf8Q9j7P0hVtVBawYaSLUCCfJ/QnH7JAP+0VRamyooZZCw==
|
integrity sha512-K9ZKWd+ewodbS/1kewedmITeeKLUQswMOXwIv8XFLPt3Ondodji0vr1XXXsttlyl+V2QG/9tYVV2RJ9Ch5LdrA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@graphiql/toolkit" "^0.11.0"
|
"@graphiql/toolkit" "^0.11.0"
|
||||||
"@headlessui/react" "^1.7.15"
|
"@headlessui/react" "^1.7.15"
|
||||||
@ -353,17 +353,17 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e"
|
resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e"
|
||||||
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==
|
integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==
|
||||||
|
|
||||||
"@orchidjs/sifter@^1.0.3":
|
"@orchidjs/sifter@^1.1.0":
|
||||||
version "1.0.3"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/@orchidjs/sifter/-/sifter-1.0.3.tgz#43f42519472282eb632d0a1589184f044d64129b"
|
resolved "https://registry.yarnpkg.com/@orchidjs/sifter/-/sifter-1.1.0.tgz#b36154ad0cda4898305d1ac44f318b41048a0438"
|
||||||
integrity sha512-zCZbwKegHytfsPm8Amcfh7v/4vHqTAaOu6xFswBYcn8nznBOuseu6COB2ON7ez0tFV0mKL0nRNnCiZZA+lU9/g==
|
integrity sha512-mYwHCfr736cIWWdhhSZvDbf90AKt2xyrJspKFC3qyIJG1LtrJeJunYEqCGG4Aq2ijENbc4WkOjszcvNaIAS/pQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@orchidjs/unicode-variants" "^1.0.4"
|
"@orchidjs/unicode-variants" "^1.1.2"
|
||||||
|
|
||||||
"@orchidjs/unicode-variants@^1.0.4":
|
"@orchidjs/unicode-variants@^1.1.2":
|
||||||
version "1.0.4"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@orchidjs/unicode-variants/-/unicode-variants-1.0.4.tgz#6d2f812e3b19545bba2d81caffff1204de9a6a58"
|
resolved "https://registry.yarnpkg.com/@orchidjs/unicode-variants/-/unicode-variants-1.1.2.tgz#1fd71791a67fdd1591ebe0dcaadd3964537a824e"
|
||||||
integrity sha512-NvVBRnZNE+dugiXERFsET1JlKZfM5lJDEpSMilKW4bToYJ7pxf0Zne78xyXB2ny2c2aHfJ6WLnz1AaTNHAmQeQ==
|
integrity sha512-5DobW1CHgnBROOEpFlEXytED5OosEWESFvg/VYmH0143oXcijYTprRYJTs+55HzGM4IqxiLFSuqEzu9mPNwVsA==
|
||||||
|
|
||||||
"@parcel/watcher-android-arm64@2.4.1":
|
"@parcel/watcher-android-arm64@2.4.1":
|
||||||
version "2.4.1"
|
version "2.4.1"
|
||||||
@ -1883,12 +1883,12 @@ graphiql-explorer@^0.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/graphiql-explorer/-/graphiql-explorer-0.9.0.tgz#25f6b990bfc3e04e88c0cf419e28d12abe2c4fbe"
|
resolved "https://registry.yarnpkg.com/graphiql-explorer/-/graphiql-explorer-0.9.0.tgz#25f6b990bfc3e04e88c0cf419e28d12abe2c4fbe"
|
||||||
integrity sha512-fZC/wsuatqiQDO2otchxriFO0LaWIo/ovF/CQJ1yOudmY0P7pzDiP+l9CEHUiWbizk3e99x6DQG4XG1VxA+d6A==
|
integrity sha512-fZC/wsuatqiQDO2otchxriFO0LaWIo/ovF/CQJ1yOudmY0P7pzDiP+l9CEHUiWbizk3e99x6DQG4XG1VxA+d6A==
|
||||||
|
|
||||||
graphiql@3.7.1:
|
graphiql@3.7.2:
|
||||||
version "3.7.1"
|
version "3.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/graphiql/-/graphiql-3.7.1.tgz#9fb727e15db443b22823389d13dc5d98c3ce0ff9"
|
resolved "https://registry.yarnpkg.com/graphiql/-/graphiql-3.7.2.tgz#6a754256f4f2e6268a64e585b0fe35bf38f1b87d"
|
||||||
integrity sha512-kmummedOrFYs0BI5evrVY0AerOYlaMt/Sc/e+Sta1x8X6vEMYWNeUUz/kKF2NQT5BcsR3FnNdFt1Gk2QMgueGQ==
|
integrity sha512-DL+KrX+aQdyzl+KwcqjlmdYdjyKegm7FcZJKkIQ1e56xn6Eoe8lw5F4t65gFex/45fHzv8e8CpaIcljxfJhO7A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@graphiql/react" "^0.26.2"
|
"@graphiql/react" "^0.27.0"
|
||||||
|
|
||||||
graphql-language-service@5.3.0, graphql-language-service@^5.3.0:
|
graphql-language-service@5.3.0, graphql-language-service@^5.3.0:
|
||||||
version "5.3.0"
|
version "5.3.0"
|
||||||
@ -1904,10 +1904,10 @@ graphql@16.9.0:
|
|||||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.9.0.tgz#1c310e63f16a49ce1fbb230bd0a000e99f6f115f"
|
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.9.0.tgz#1c310e63f16a49ce1fbb230bd0a000e99f6f115f"
|
||||||
integrity sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==
|
integrity sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==
|
||||||
|
|
||||||
gridstack@10.3.1:
|
gridstack@11.1.2:
|
||||||
version "10.3.1"
|
version "11.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-10.3.1.tgz#4ed704279c40094fc1b9e3318f20b573f2fe9f40"
|
resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-11.1.2.tgz#e72091e2883f7b37cbd150c218d38eebc9fc4f18"
|
||||||
integrity sha512-Ra82k/88gdeiu3ZP40COS4bI4sGhNQlZAaAQ6szfPfr68zVpsXxiyLKr5zYcTpKX4jjcwyNsNNdcV1tDJc71fA==
|
integrity sha512-6wJ5RffnFchF63/Yhs6tcZcWxRG1EgCnxgejbQsAjQ6Qj8QqKjew73jPq5c1yCAiyEAsXxI2tOJ8lZABOAZxoQ==
|
||||||
|
|
||||||
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
has-bigints@^1.0.1, has-bigints@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
@ -1970,6 +1970,11 @@ immutable@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381"
|
||||||
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==
|
||||||
|
|
||||||
|
immutable@^5.0.2:
|
||||||
|
version "5.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.0.3.tgz#aa037e2313ea7b5d400cd9298fa14e404c933db1"
|
||||||
|
integrity sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==
|
||||||
|
|
||||||
import-fresh@^3.2.1:
|
import-fresh@^3.2.1:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
|
||||||
@ -2656,15 +2661,16 @@ safe-regex-test@^1.0.3:
|
|||||||
es-errors "^1.3.0"
|
es-errors "^1.3.0"
|
||||||
is-regex "^1.1.4"
|
is-regex "^1.1.4"
|
||||||
|
|
||||||
sass@1.80.5:
|
sass@1.82.0:
|
||||||
version "1.80.5"
|
version "1.82.0"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.80.5.tgz#0ba965223d44df22497f2966b498cf5c453fae8f"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70"
|
||||||
integrity sha512-TQd2aoQl/+zsxRMEDSxVdpPIqeq9UFc6pr7PzkugiTx3VYCFPUaa3P4RrBQsqok4PO200Vkz0vXQBNlg7W907g==
|
integrity sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@parcel/watcher" "^2.4.1"
|
|
||||||
chokidar "^4.0.0"
|
chokidar "^4.0.0"
|
||||||
immutable "^4.0.0"
|
immutable "^5.0.2"
|
||||||
source-map-js ">=0.6.2 <2.0.0"
|
source-map-js ">=0.6.2 <2.0.0"
|
||||||
|
optionalDependencies:
|
||||||
|
"@parcel/watcher" "^2.4.1"
|
||||||
|
|
||||||
sass@^1.71.1:
|
sass@^1.71.1:
|
||||||
version "1.77.8"
|
version "1.77.8"
|
||||||
@ -2864,13 +2870,13 @@ toggle-selection@^1.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||||
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
||||||
|
|
||||||
tom-select@2.3.1:
|
tom-select@2.4.1:
|
||||||
version "2.3.1"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.3.1.tgz#df338d9082874cd0bceb3bee87ed0184447c47f1"
|
resolved "https://registry.yarnpkg.com/tom-select/-/tom-select-2.4.1.tgz#6a0b6df8af3df7b09b22dd965eb75ce4d1c547bc"
|
||||||
integrity sha512-QS4vnOcB6StNGqX4sGboGXL2fkhBF2gIBB+8Hwv30FZXYPn0CyYO8kkdATRvwfCTThxiR4WcXwKJZ3cOmtI9eg==
|
integrity sha512-adI8H8+wk8RRzHYLQ3bXSk2Q+FAq/kzAATrcWlJ2fbIrEzb0VkwaXzKHTAlBwSJrhqbPJvhV/0eypFkED/nAug==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@orchidjs/sifter" "^1.0.3"
|
"@orchidjs/sifter" "^1.1.0"
|
||||||
"@orchidjs/unicode-variants" "^1.0.4"
|
"@orchidjs/unicode-variants" "^1.1.2"
|
||||||
|
|
||||||
ts-api-utils@^1.3.0:
|
ts-api-utils@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
version: "4.1.7"
|
version: "4.1.8"
|
||||||
edition: "Community"
|
edition: "Community"
|
||||||
published: "2024-11-21"
|
published: "2024-12-12"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% block title %}{{ object }} - {% trans "Config" %}{% endblock %}
|
{% block title %}{{ object }} - {% trans "Config" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Config Template" %}</h2>
|
<h2 class="card-header">{% trans "Config Template" %}</h2>
|
||||||
@ -48,19 +48,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
{% if config_template %}
|
||||||
<h2 class="card-header d-flex justify-content-between">
|
{% if rendered_config %}
|
||||||
{% trans "Rendered Config" %}
|
<div class="card">
|
||||||
<a href="?export=True" class="btn btn-primary lh-1" role="button">
|
<h2 class="card-header d-flex justify-content-between">
|
||||||
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
|
{% trans "Rendered Config" %}
|
||||||
</a>
|
<a href="?export=True" class="btn btn-primary lh-1" role="button">
|
||||||
</h2>
|
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
|
||||||
{% if config_template %}
|
</a>
|
||||||
<pre class="card-body">{{ rendered_config }}</pre>
|
</h2>
|
||||||
|
<pre class="card-body">{{ rendered_config }}</pre>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card-body text-muted">{% trans "No configuration template found" %}</div>
|
<div class="alert alert-warning">
|
||||||
|
<h4 class="alert-title mb-1">{% trans "Error rendering template" %}</h4>
|
||||||
|
{% trans error_message %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% trans "No configuration template has been assigned for this device." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
{% block title %}{{ object }} - {% trans "Config" %}{% endblock %}
|
{% block title %}{{ object }} - {% trans "Config" %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row mb-3">
|
<div class="row">
|
||||||
<div class="col-5">
|
<div class="col-5">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 class="card-header">{% trans "Config Template" %}</h2>
|
<h2 class="card-header">{% trans "Config Template" %}</h2>
|
||||||
@ -48,19 +48,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
{% if config_template %}
|
||||||
<h2 class="card-header d-flex justify-content-between">
|
{% if rendered_config %}
|
||||||
{% trans "Rendered Config" %}
|
<div class="card">
|
||||||
<a href="?export=True" class="btn btn-primary lh-1" role="button">
|
<h2 class="card-header d-flex justify-content-between">
|
||||||
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
|
{% trans "Rendered Config" %}
|
||||||
</a>
|
<a href="?export=True" class="btn btn-primary lh-1" role="button">
|
||||||
</h2>
|
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
|
||||||
{% if config_template %}
|
</a>
|
||||||
<pre class="card-body">{{ rendered_config }}</pre>
|
</h2>
|
||||||
|
<pre class="card-body">{{ rendered_config }}</pre>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="card-body text-muted">{% trans "No configuration template found" %}</div>
|
<div class="alert alert-warning">
|
||||||
|
<h4 class="alert-title mb-1">{% trans "Error rendering template" %}</h4>
|
||||||
|
{% trans error_message %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
{% else %}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% trans "No configuration template has been assigned for this virtual machine." %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -144,6 +144,19 @@ class APIPaginationTestCase(APITestCase):
|
|||||||
self.assertIsNone(response.data['previous'])
|
self.assertIsNone(response.data['previous'])
|
||||||
self.assertEqual(len(response.data['results']), page_size)
|
self.assertEqual(len(response.data['results']), page_size)
|
||||||
|
|
||||||
|
@override_settings(MAX_PAGE_SIZE=30)
|
||||||
|
def test_default_page_size_with_small_max_page_size(self):
|
||||||
|
response = self.client.get(self.url, format='json', **self.header)
|
||||||
|
page_size = get_config().MAX_PAGE_SIZE
|
||||||
|
paginate_count = get_config().PAGINATE_COUNT
|
||||||
|
self.assertLess(page_size, 100, "Default page size not sufficient for data set")
|
||||||
|
|
||||||
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['count'], 100)
|
||||||
|
self.assertTrue(response.data['next'].endswith(f'?limit={paginate_count}&offset={paginate_count}'))
|
||||||
|
self.assertIsNone(response.data['previous'])
|
||||||
|
self.assertEqual(len(response.data['results']), paginate_count)
|
||||||
|
|
||||||
def test_custom_page_size(self):
|
def test_custom_page_size(self):
|
||||||
response = self.client.get(f'{self.url}?limit=10', format='json', **self.header)
|
response = self.client.get(f'{self.url}?limit=10', format='json', **self.header)
|
||||||
|
|
||||||
@ -153,15 +166,15 @@ class APIPaginationTestCase(APITestCase):
|
|||||||
self.assertIsNone(response.data['previous'])
|
self.assertIsNone(response.data['previous'])
|
||||||
self.assertEqual(len(response.data['results']), 10)
|
self.assertEqual(len(response.data['results']), 10)
|
||||||
|
|
||||||
@override_settings(MAX_PAGE_SIZE=20)
|
@override_settings(MAX_PAGE_SIZE=80)
|
||||||
def test_max_page_size(self):
|
def test_max_page_size(self):
|
||||||
response = self.client.get(f'{self.url}?limit=0', format='json', **self.header)
|
response = self.client.get(f'{self.url}?limit=0', format='json', **self.header)
|
||||||
|
|
||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data['count'], 100)
|
self.assertEqual(response.data['count'], 100)
|
||||||
self.assertTrue(response.data['next'].endswith('?limit=20&offset=20'))
|
self.assertTrue(response.data['next'].endswith('?limit=80&offset=80'))
|
||||||
self.assertIsNone(response.data['previous'])
|
self.assertIsNone(response.data['previous'])
|
||||||
self.assertEqual(len(response.data['results']), 20)
|
self.assertEqual(len(response.data['results']), 80)
|
||||||
|
|
||||||
@override_settings(MAX_PAGE_SIZE=0)
|
@override_settings(MAX_PAGE_SIZE=0)
|
||||||
def test_max_page_size_disabled(self):
|
def test_max_page_size_disabled(self):
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import traceback
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Prefetch, Sum
|
from django.db.models import Prefetch, Sum
|
||||||
@ -425,7 +423,8 @@ class VirtualMachineRenderConfigView(generic.ObjectView):
|
|||||||
# If a direct export has been requested, return the rendered template content as a
|
# If a direct export has been requested, return the rendered template content as a
|
||||||
# downloadable file.
|
# downloadable file.
|
||||||
if request.GET.get('export'):
|
if request.GET.get('export'):
|
||||||
response = HttpResponse(context['rendered_config'], content_type='text')
|
content = context['rendered_config'] or context['error_message']
|
||||||
|
response = HttpResponse(content, content_type='text')
|
||||||
filename = f"{instance.name or 'config'}.txt"
|
filename = f"{instance.name or 'config'}.txt"
|
||||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||||
return response
|
return response
|
||||||
@ -443,17 +442,18 @@ class VirtualMachineRenderConfigView(generic.ObjectView):
|
|||||||
|
|
||||||
# Render the config template
|
# Render the config template
|
||||||
rendered_config = None
|
rendered_config = None
|
||||||
|
error_message = None
|
||||||
if config_template := instance.get_config_template():
|
if config_template := instance.get_config_template():
|
||||||
try:
|
try:
|
||||||
rendered_config = config_template.render(context=context_data)
|
rendered_config = config_template.render(context=context_data)
|
||||||
except TemplateError as e:
|
except TemplateError as e:
|
||||||
messages.error(request, _("An error occurred while rendering the template: {error}").format(error=e))
|
error_message = _("An error occurred while rendering the template: {error}").format(error=e)
|
||||||
rendered_config = traceback.format_exc()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'config_template': config_template,
|
'config_template': config_template,
|
||||||
'context_data': context_data,
|
'context_data': context_data,
|
||||||
'rendered_config': rendered_config,
|
'rendered_config': rendered_config,
|
||||||
|
'error_message': error_message,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django==5.0.9
|
Django==5.0.10
|
||||||
django-cors-headers==4.6.0
|
django-cors-headers==4.6.0
|
||||||
django-debug-toolbar==4.4.6
|
django-debug-toolbar==4.4.6
|
||||||
django-filter==24.3
|
django-filter==24.3
|
||||||
@ -14,16 +14,16 @@ django-taggit==6.1.0
|
|||||||
django-tables2==2.7.0
|
django-tables2==2.7.0
|
||||||
django-timezone-field==7.0
|
django-timezone-field==7.0
|
||||||
djangorestframework==3.15.2
|
djangorestframework==3.15.2
|
||||||
drf-spectacular==0.27.2
|
drf-spectacular==0.28.0
|
||||||
drf-spectacular-sidecar==2024.11.1
|
drf-spectacular-sidecar==2024.12.1
|
||||||
feedparser==6.0.11
|
feedparser==6.0.11
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
Jinja2==3.1.4
|
Jinja2==3.1.4
|
||||||
Markdown==3.7
|
Markdown==3.7
|
||||||
mkdocs-material==9.5.45
|
mkdocs-material==9.5.48
|
||||||
mkdocstrings[python-legacy]==0.27.0
|
mkdocstrings[python-legacy]==0.27.0
|
||||||
netaddr==1.3.0
|
netaddr==1.3.0
|
||||||
nh3==0.2.18
|
nh3==0.2.19
|
||||||
Pillow==11.0.0
|
Pillow==11.0.0
|
||||||
psycopg[c,pool]==3.2.3
|
psycopg[c,pool]==3.2.3
|
||||||
PyYAML==6.0.2
|
PyYAML==6.0.2
|
||||||
@ -31,8 +31,8 @@ requests==2.32.3
|
|||||||
rq==2.0
|
rq==2.0
|
||||||
social-auth-app-django==5.4.2
|
social-auth-app-django==5.4.2
|
||||||
social-auth-core==4.5.4
|
social-auth-core==4.5.4
|
||||||
strawberry-graphql==0.251.0
|
strawberry-graphql==0.253.1
|
||||||
strawberry-graphql-django==0.50.0
|
strawberry-graphql-django==0.51.0
|
||||||
svgwrite==1.4.3
|
svgwrite==1.4.3
|
||||||
tablib==3.7.0
|
tablib==3.7.0
|
||||||
tzdata==2024.2
|
tzdata==2024.2
|
||||||
|
Loading…
Reference in New Issue
Block a user