Просмотр исходного кода

Merge branch 'feature' into 6829-graphql-reverse-relations

jeremystretch 4 лет назад
Родитель
Сommit
1b612816cc
37 измененных файлов с 690 добавлено и 352 удалено
  1. 20 3
      netbox/circuits/forms.py
  2. 103 8
      netbox/dcim/forms.py
  3. 25 13
      netbox/extras/choices.py
  4. 44 2
      netbox/extras/forms.py
  5. 2 2
      netbox/extras/templatetags/custom_links.py
  6. 4 4
      netbox/extras/tests/test_views.py
  7. 59 8
      netbox/ipam/forms.py
  8. 0 0
      netbox/project-static/dist/netbox-dark.css
  9. 0 0
      netbox/project-static/dist/netbox-light.css
  10. 128 39
      netbox/project-static/styles/netbox.scss
  11. 8 0
      netbox/project-static/styles/theme-dark.scss
  12. 14 0
      netbox/project-static/styles/theme-light.scss
  13. 14 14
      netbox/templates/circuits/inc/circuit_termination.html
  14. 167 193
      netbox/templates/dcim/device/base.html
  15. 1 1
      netbox/templates/dcim/devicetype.html
  16. 1 1
      netbox/templates/dcim/interface.html
  17. 3 3
      netbox/templates/dcim/rack.html
  18. 5 5
      netbox/templates/dcim/rack_elevation_list.html
  19. 11 9
      netbox/templates/generic/object.html
  20. 6 4
      netbox/templates/generic/object_edit.html
  21. 2 2
      netbox/templates/generic/object_list.html
  22. 6 3
      netbox/templates/inc/filter_list.html
  23. 4 3
      netbox/templates/ipam/inc/toggle_available.html
  24. 1 1
      netbox/templates/ipam/iprange/ip_addresses.html
  25. 1 1
      netbox/templates/ipam/prefix/ip_addresses.html
  26. 2 2
      netbox/templates/ipam/prefix_list.html
  27. 20 19
      netbox/templates/virtualization/cluster/base.html
  28. 2 2
      netbox/templates/virtualization/virtualmachine/base.html
  29. 6 0
      netbox/tenancy/forms.py
  30. 5 2
      netbox/utilities/templates/buttons/add.html
  31. 1 1
      netbox/utilities/templates/buttons/clone.html
  32. 1 1
      netbox/utilities/templates/buttons/delete.html
  33. 1 1
      netbox/utilities/templates/buttons/edit.html
  34. 2 2
      netbox/utilities/templates/buttons/export.html
  35. 1 1
      netbox/utilities/templates/buttons/import.html
  36. 1 1
      netbox/utilities/templates/search/searchbar.html
  37. 19 1
      netbox/virtualization/forms.py

+ 20 - 3
netbox/circuits/forms.py

@@ -107,9 +107,15 @@ class ProviderBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBu
 class ProviderFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = Provider
     field_groups = [
+        ['q'],
         ['region_id', 'site_id'],
         ['asn', 'tag'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -196,7 +202,12 @@ class ProviderNetworkBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
 
 class ProviderNetworkFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = ProviderNetwork
-    field_order = ['provider_id']
+    field_order = ['q', 'provider_id', 'tag']
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     provider_id = DynamicModelMultipleChoiceField(
         queryset=Provider.objects.all(),
         required=False,
@@ -358,16 +369,22 @@ class CircuitBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul
 class CircuitFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = Circuit
     field_order = [
-        'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id', 'tenant_id',
-        'commit_rate',
+        'q', 'type_id', 'provider_id', 'provider_network_id', 'status', 'region_id', 'site_id', 'tenant_group_id',
+        'tenant_id', 'commit_rate',
     ]
     field_groups = [
+        ['q'],
         ['type_id', 'status', 'commit_rate'],
         ['provider_id', 'provider_network_id'],
         ['region_id', 'site_id'],
         ['tenant_group_id', 'tenant_id'],
         ['tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     type_id = DynamicModelMultipleChoiceField(
         queryset=CircuitType.objects.all(),
         required=False,

+ 103 - 8
netbox/dcim/forms.py

@@ -56,12 +56,18 @@ def get_device_by_name_or_pk(name):
 
 class DeviceComponentFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     field_order = [
-        'name', 'label', 'region_id', 'site_group_id', 'site_id',
+        'q', 'name', 'label', 'region_id', 'site_group_id', 'site_id',
     ]
     field_groups = [
+        ['q'],
         ['name', 'label'],
         ['region_id', 'site_group_id', 'site_id'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     name = forms.CharField(
         required=False
     )
@@ -452,12 +458,18 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
 
 class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = Site
-    field_order = ['status', 'region_id', 'tenant_group_id', 'tenant_id']
+    field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id']
     field_groups = [
+        ['q'],
         ['status', 'region_id'],
         ['tenant_group_id', 'tenant_id'],
         ['tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     status = forms.MultipleChoiceField(
         choices=SiteStatusChoices,
         required=False,
@@ -568,6 +580,11 @@ class LocationBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
 
 class LocationFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = Location
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -862,12 +879,18 @@ class RackBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
 
 class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = Rack
-    field_order = ['region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
+    field_order = ['q', 'region_id', 'site_id', 'location_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id']
     field_groups = [
+        ['q'],
         ['status', 'role_id'],
         ['region_id', 'site_id', 'location_id'],
         ['tenant_group_id', 'tenant_id'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -927,7 +950,8 @@ class RackFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
 
 class RackElevationFilterForm(RackFilterForm):
     field_order = [
-        'region_id', 'site_id', 'location_id', 'id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
+        'q', 'region_id', 'site_id', 'location_id', 'id', 'status', 'role_id', 'tenant_group_id',
+        'tenant_id',
     ]
     id = DynamicModelMultipleChoiceField(
         queryset=Rack.objects.all(),
@@ -1092,11 +1116,17 @@ class RackReservationBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomField
 
 class RackReservationFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = RackReservation
-    field_order = ['region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id']
+    field_order = ['q', 'region_id', 'site_id', 'location_id', 'user_id', 'tenant_group_id', 'tenant_id']
     field_groups = [
+        ['q'],
         ['region_id', 'site_id', 'location_id'],
         ['user_id', 'tenant_group_id', 'tenant_id'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -1246,12 +1276,18 @@ class DeviceTypeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
 class DeviceTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = DeviceType
     field_groups = [
+        ['q'],
         ['manufacturer_id', 'subdevice_role'],
         ['console_ports', 'console_server_ports'],
         ['power_ports', 'power_outlets'],
         ['interfaces', 'pass_through_ports'],
         ['tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
@@ -2058,6 +2094,11 @@ class PlatformBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
 
 class PlatformFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = Platform
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     manufacturer_id = DynamicModelMultipleChoiceField(
         queryset=Manufacturer.objects.all(),
         required=False,
@@ -2465,16 +2506,23 @@ class DeviceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
 class DeviceFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm):
     model = Device
     field_order = [
-        'region_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
-        'manufacturer_id', 'device_type_id', 'asset_tag', 'mac_address', 'has_primary_ip',
+        'q', 'region_id', 'site_id', 'location_id', 'rack_id', 'status', 'role_id',
+        'tenant_group_id', 'tenant_id', 'manufacturer_id', 'device_type_id', 'asset_tag',
+        'mac_address', 'has_primary_ip',
     ]
     field_groups = [
+        ['q'],
         ['region_id', 'site_id', 'location_id', 'rack_id'],
         ['status', 'role_id', 'asset_tag'],
         ['tenant_group_id', 'tenant_id'],
         ['manufacturer_id', 'device_type_id'],
         ['mac_address', 'has_primary_ip'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -2654,6 +2702,7 @@ class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentFor
 class ConsolePortFilterForm(DeviceComponentFilterForm):
     model = ConsolePort
     field_groups = [
+        ['q'],
         ['name', 'label'],
         ['type', 'speed'],
         ['region_id', 'site_group_id', 'site_id'],
@@ -2761,6 +2810,7 @@ class ConsolePortCSVForm(CustomFieldModelCSVForm):
 class ConsoleServerPortFilterForm(DeviceComponentFilterForm):
     model = ConsoleServerPort
     field_groups = [
+        ['q'],
         ['name', 'label'],
         ['type', 'speed'],
         ['region_id', 'site_group_id', 'site_id'],
@@ -2868,6 +2918,7 @@ class ConsoleServerPortCSVForm(CustomFieldModelCSVForm):
 class PowerPortFilterForm(DeviceComponentFilterForm):
     model = PowerPort
     field_groups = [
+        ['q'],
         ['name', 'label', 'type'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tag'],
@@ -2973,6 +3024,7 @@ class PowerPortCSVForm(CustomFieldModelCSVForm):
 class PowerOutletFilterForm(DeviceComponentFilterForm):
     model = PowerOutlet
     field_groups = [
+        ['q'],
         ['name', 'label', 'type'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tag'],
@@ -3145,6 +3197,7 @@ class PowerOutletCSVForm(CustomFieldModelCSVForm):
 class InterfaceFilterForm(DeviceComponentFilterForm):
     model = Interface
     field_groups = [
+        ['q'],
         ['name', 'label', 'type', 'enabled'],
         ['mgmt_only', 'mac_address'],
         ['region_id', 'site_group_id', 'site_id'],
@@ -3493,6 +3546,7 @@ class InterfaceCSVForm(CustomFieldModelCSVForm):
 
 class FrontPortFilterForm(DeviceComponentFilterForm):
     field_groups = [
+        ['q'],
         ['name', 'label', 'type', 'color'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tag']
@@ -3682,6 +3736,7 @@ class FrontPortCSVForm(CustomFieldModelCSVForm):
 class RearPortFilterForm(DeviceComponentFilterForm):
     model = RearPort
     field_groups = [
+        ['q'],
         ['name', 'label', 'type', 'color'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tag']
@@ -3783,6 +3838,7 @@ class RearPortCSVForm(CustomFieldModelCSVForm):
 class DeviceBayFilterForm(DeviceComponentFilterForm):
     model = DeviceBay
     field_groups = [
+        ['q'],
         ['name', 'label'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tag']
@@ -4013,6 +4069,7 @@ class InventoryItemBulkEditForm(
 class InventoryItemFilterForm(DeviceComponentFilterForm):
     model = InventoryItem
     field_groups = [
+        ['q'],
         ['name', 'label', 'manufacturer_id'],
         ['serial', 'asset_tag', 'discovered'],
         ['region_id', 'site_group_id', 'site_id'],
@@ -4488,11 +4545,17 @@ class CableBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkE
 class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = Cable
     field_groups = [
+        ['q'],
         ['type', 'status', 'color'],
         ['device_id', 'rack_id'],
         ['region_id', 'site_id', 'tenant_id'],
         ['tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -4556,6 +4619,11 @@ class CableFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
 #
 
 class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -4583,6 +4651,11 @@ class ConsoleConnectionFilterForm(BootstrapMixin, forms.Form):
 
 
 class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -4610,6 +4683,11 @@ class PowerConnectionFilterForm(BootstrapMixin, forms.Form):
 
 
 class InterfaceConnectionFilterForm(BootstrapMixin, forms.Form):
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -4877,12 +4955,18 @@ class VirtualChassisCSVForm(CustomFieldModelCSVForm):
 
 class VirtualChassisFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = VirtualChassis
-    field_order = ['region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id']
+    field_order = ['q', 'region_id', 'site_group_id', 'site_id', 'tenant_group_id', 'tenant_id']
     field_groups = [
+        ['q'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tenant_group_id', 'tenant_id'],
         ['tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -5022,6 +5106,11 @@ class PowerPanelBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModel
 
 class PowerPanelFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = PowerPanel
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -5260,12 +5349,18 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
 class PowerFeedFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = PowerFeed
     field_groups = [
+        ['q'],
         ['region_id', 'site_group_id', 'site_id'],
         ['power_panel_id', 'rack_id'],
         ['type', 'supply', 'max_utilization'],
         ['phase', 'voltage', 'amperage'],
         ['status', 'tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,

+ 25 - 13
netbox/extras/choices.py

@@ -46,28 +46,40 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
 class CustomLinkButtonClassChoices(ChoiceSet):
 
     CLASS_DEFAULT = 'outline-dark'
-    CLASS_PRIMARY = 'primary'
-    CLASS_SUCCESS = 'success'
-    CLASS_INFO = 'info'
-    CLASS_WARNING = 'warning'
-    CLASS_DANGER = 'danger'
-    CLASS_LINK = 'link'
+    CLASS_LINK = 'ghost-dark'
+    CLASS_BLUE = 'blue'
+    CLASS_INDIGO = 'indigo'
+    CLASS_PURPLE = 'purple'
+    CLASS_PINK = 'pink'
+    CLASS_RED = 'red'
+    CLASS_ORANGE = 'orange'
+    CLASS_YELLOW = 'yellow'
+    CLASS_GREEN = 'green'
+    CLASS_TEAL = 'teal'
+    CLASS_CYAN = 'cyan'
+    CLASS_GRAY = 'secondary'
 
     CHOICES = (
         (CLASS_DEFAULT, 'Default'),
-        (CLASS_PRIMARY, 'Primary (blue)'),
-        (CLASS_SUCCESS, 'Success (green)'),
-        (CLASS_INFO, 'Info (aqua)'),
-        (CLASS_WARNING, 'Warning (orange)'),
-        (CLASS_DANGER, 'Danger (red)'),
-        (CLASS_LINK, 'None (link)'),
+        (CLASS_LINK, 'Link'),
+        (CLASS_BLUE, 'Blue'),
+        (CLASS_INDIGO, 'Indigo'),
+        (CLASS_PURPLE, 'Purple'),
+        (CLASS_PINK, 'Pink'),
+        (CLASS_RED, 'Red'),
+        (CLASS_ORANGE, 'Orange'),
+        (CLASS_YELLOW, 'Yellow'),
+        (CLASS_GREEN, 'Green'),
+        (CLASS_TEAL, 'Teal'),
+        (CLASS_CYAN, 'Cyan'),
+        (CLASS_GRAY, 'Gray'),
     )
 
-
 #
 # ObjectChanges
 #
 
+
 class ObjectChangeActionChoices(ChoiceSet):
 
     ACTION_CREATE = 'create'

+ 44 - 2
netbox/extras/forms.py

@@ -77,9 +77,15 @@ class CustomFieldBulkEditForm(BootstrapMixin, BulkEditForm):
 
 class CustomFieldFilterForm(BootstrapMixin, forms.Form):
     field_groups = [
+        ['q'],
         ['type', 'content_types'],
         ['weight', 'required'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     content_types = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields')
@@ -167,9 +173,15 @@ class CustomLinkBulkEditForm(BootstrapMixin, BulkEditForm):
 
 class CustomLinkFilterForm(BootstrapMixin, forms.Form):
     field_groups = [
+        ['q'],
         ['content_type'],
         ['weight', 'new_window'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     content_type = ContentTypeChoiceField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields')
@@ -252,9 +264,15 @@ class ExportTemplateBulkEditForm(BootstrapMixin, BulkEditForm):
 
 class ExportTemplateFilterForm(BootstrapMixin, forms.Form):
     field_groups = [
+        ['q'],
         ['content_type', 'mime_type'],
         ['file_extension', 'as_attachment'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     content_type = ContentTypeChoiceField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields')
@@ -358,9 +376,15 @@ class WebhookBulkEditForm(BootstrapMixin, BulkEditForm):
 
 class WebhookFilterForm(BootstrapMixin, forms.Form):
     field_groups = [
+        ['q'],
         ['content_types', 'http_method'],
         ['enabled', 'type_create', 'type_update', 'type_delete'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     content_types = ContentTypeMultipleChoiceField(
         queryset=ContentType.objects.all(),
         limit_choices_to=FeatureQuery('custom_fields')
@@ -664,15 +688,21 @@ class ConfigContextBulkEditForm(BootstrapMixin, BulkEditForm):
 
 class ConfigContextFilterForm(BootstrapMixin, forms.Form):
     field_order = [
-        'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id', 'cluster_id',
-        'tenant_group_id', 'tenant_id',
+        'q', 'region_id', 'site_group_id', 'site_id', 'role_id', 'platform_id', 'cluster_group_id',
+        'cluster_id', 'tenant_group_id', 'tenant_id',
     ]
     field_groups = [
+        ['q'],
         ['region_id', 'site_group_id', 'site_id'],
         ['device_type_id', 'role_id', 'platform_id'],
         ['cluster_group_id', 'cluster_id'],
         ['tenant_group_id', 'tenant_id', 'tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -812,9 +842,15 @@ class JournalEntryBulkEditForm(BootstrapMixin, BulkEditForm):
 class JournalEntryFilterForm(BootstrapMixin, forms.Form):
     model = JournalEntry
     field_groups = [
+        ['q'],
         ['created_before', 'created_after', 'created_by_id'],
         ['assigned_object_type_id', 'kind']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     created_after = forms.DateTimeField(
         required=False,
         label=_('After'),
@@ -857,9 +893,15 @@ class JournalEntryFilterForm(BootstrapMixin, forms.Form):
 class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
     model = ObjectChange
     field_groups = [
+        ['q'],
         ['time_before', 'time_after', 'action'],
         ['user_id', 'changed_object_type_id'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     time_after = forms.DateTimeField(
         required=False,
         label=_('After'),

+ 2 - 2
netbox/extras/templatetags/custom_links.py

@@ -10,10 +10,10 @@ from utilities.utils import render_jinja2
 
 register = template.Library()
 
-LINK_BUTTON = '<a href="{}"{} class="btn btn-sm btn-{} m-1">{}</a>\n'
+LINK_BUTTON = '<a href="{}"{} class="btn btn-sm btn-{}">{}</a>\n'
 
 GROUP_BUTTON = """
-<div class="dropdown m-1">
+<div class="dropdown">
     <button
         class="btn btn-sm btn-{} dropdown-toggle"
         type="button"

+ 4 - 4
netbox/extras/tests/test_views.py

@@ -75,13 +75,13 @@ class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
 
         cls.csv_data = (
             "name,content_type,weight,button_class,link_text,link_url",
-            "Custom Link 4,dcim.site,100,primary,Link 4,http://exmaple.com/?4",
-            "Custom Link 5,dcim.site,100,primary,Link 5,http://exmaple.com/?5",
-            "Custom Link 6,dcim.site,100,primary,Link 6,http://exmaple.com/?6",
+            "Custom Link 4,dcim.site,100,blue,Link 4,http://exmaple.com/?4",
+            "Custom Link 5,dcim.site,100,blue,Link 5,http://exmaple.com/?5",
+            "Custom Link 6,dcim.site,100,blue,Link 6,http://exmaple.com/?6",
         )
 
         cls.bulk_edit_data = {
-            'button_class': CustomLinkButtonClassChoices.CLASS_INFO,
+            'button_class': CustomLinkButtonClassChoices.CLASS_CYAN,
             'weight': 200,
         }
 

+ 59 - 8
netbox/ipam/forms.py

@@ -106,12 +106,18 @@ class VRFBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEdi
 
 class VRFFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = VRF
-    field_order = ['import_target_id', 'export_target_id', 'tenant_group_id', 'tenant_id']
+    field_order = ['q', 'import_target_id', 'export_target_id', 'tenant_group_id', 'tenant_id']
     field_groups = [
+        ['q'],
         ['import_target_id', 'export_target_id'],
         ['tenant_group_id', 'tenant_id'],
         ['tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     import_target_id = DynamicModelMultipleChoiceField(
         queryset=RouteTarget.objects.all(),
         required=False,
@@ -179,11 +185,17 @@ class RouteTargetBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldMode
 
 class RouteTargetFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = RouteTarget
-    field_order = ['name', 'tenant_group_id', 'tenant_id', 'importing_vrfs', 'exporting_vrfs']
+    field_order = ['q', 'name', 'tenant_group_id', 'tenant_id', 'importing_vrfs', 'exporting_vrfs']
     field_groups = [
+        ['q'],
         ['importing_vrf_id', 'exporting_vrf_id'],
         ['tenant_group_id', 'tenant_id'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     importing_vrf_id = DynamicModelMultipleChoiceField(
         queryset=VRF.objects.all(),
         required=False,
@@ -335,11 +347,17 @@ class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelB
 
 class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = Aggregate
-    field_order = ['family', 'rir', 'tenant_group_id', 'tenant_id']
+    field_order = ['q', 'family', 'rir', 'tenant_group_id', 'tenant_id']
     field_groups = [
+        ['q'],
         ['family', 'rir_id'],
         ['tenant_group_id', 'tenant_id']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     family = forms.ChoiceField(
         required=False,
         choices=add_blank_choice(IPAddressFamilyChoices),
@@ -610,15 +628,22 @@ class PrefixBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulk
 class PrefixFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = Prefix
     field_order = [
-        'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'region_id',
-        'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id', 'is_pool', 'mark_utilized',
+        'q', 'within_include', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status',
+        'region_id', 'site_group_id', 'site_id', 'role_id', 'tenant_group_id', 'tenant_id',
+        'is_pool', 'mark_utilized',
     ]
     field_groups = [
+        ['q'],
         ['role_id', 'within_include', 'family', 'mask_length'],
         ['vrf_id', 'present_in_vrf_id', 'is_pool', 'mark_utilized'],
         ['region_id', 'site_group_id', 'site_id'],
         ['tenant_group_id', 'tenant_id', 'status', 'tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     mask_length__lte = forms.IntegerField(
         widget=forms.HiddenInput()
     )
@@ -813,12 +838,18 @@ class IPRangeBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul
 class IPRangeFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = IPRange
     field_order = [
-        'family', 'vrf_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
+        'q', 'family', 'vrf_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
     ]
     field_groups = [
+        ['q'],
         ['family', 'vrf_id', 'status', 'role_id'],
         ['tenant_group_id', 'tenant_id', 'tag'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     family = forms.ChoiceField(
         required=False,
         choices=add_blank_choice(IPAddressFamilyChoices),
@@ -1244,15 +1275,21 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form):
 class IPAddressFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = IPAddress
     field_order = [
-        'parent', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'role',
+        'q', 'parent', 'family', 'mask_length', 'vrf_id', 'present_in_vrf_id', 'status', 'role',
         'assigned_to_interface', 'tenant_group_id', 'tenant_id',
     ]
     field_groups = [
+        ['q'],
         ['parent', 'family', 'mask_length'],
         ['status', 'vrf_id', 'present_in_vrf_id'],
         ['role', 'assigned_to_interface'],
         ['tenant_group_id', 'tenant_id'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     parent = forms.CharField(
         required=False,
         widget=forms.TextInput(
@@ -1444,11 +1481,13 @@ class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
 
 class VLANGroupFilterForm(BootstrapMixin, forms.Form):
     field_groups = [
+        ['q'],
         ['region', 'sitegroup', 'site'],
         ['location', 'rack']
     ]
     q = forms.CharField(
         required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
         label=_('Search')
     )
     region = DynamicModelMultipleChoiceField(
@@ -1662,13 +1701,20 @@ class VLANBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
 class VLANFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = VLAN
     field_order = [
-        'region_id', 'site_group_id', 'site_id', 'group_id', 'status', 'role_id', 'tenant_group_id', 'tenant_id',
+        'q', 'region_id', 'site_group_id', 'site_id', 'group_id', 'status', 'role_id',
+        'tenant_group_id', 'tenant_id',
     ]
     field_groups = [
+        ['q'],
         ['region_id', 'site_group_id', 'site_id'],
         ['group_id', 'role_id', 'status'],
         ['tenant_group_id', 'tenant_id'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     region_id = DynamicModelMultipleChoiceField(
         queryset=Region.objects.all(),
         required=False,
@@ -1765,6 +1811,11 @@ class ServiceForm(BootstrapMixin, CustomFieldModelForm):
 
 class ServiceFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = Service
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     protocol = forms.ChoiceField(
         choices=add_blank_choice(ServiceProtocolChoices),
         required=False,

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-dark.css


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox-light.css


+ 128 - 39
netbox/project-static/styles/netbox.scss

@@ -89,8 +89,13 @@ table td > .progress {
   min-width: 6rem;
 }
 
-// Automatically space out adjacent columns.
-.col:not(:last-child):not(:only-child) {
+// Override Bootstrap form-control font-size when contained by .small element.
+.small .form-control {
+  font-size: $font-size-sm;
+}
+
+// Automatically space out adjacent columns, but not within card bodies.
+:not(.card-body) > .col:not(:last-child):not(:only-child) {
   margin-bottom: $spacer;
 }
 
@@ -113,15 +118,6 @@ table td > .progress {
   }
 }
 
-.search-container {
-  display: flex;
-  width: 100%;
-
-  @include media-breakpoint-down(lg) {
-    display: none;
-  }
-}
-
 .card > .table.table-flush {
   margin-bottom: 0;
   overflow: hidden;
@@ -265,24 +261,126 @@ div.title-container {
   }
 }
 
+// Object list control buttons (Add/Clone/Import/Export)
+.controls {
+  margin-bottom: map.get($spacers, 2);
+
+  // Each group of buttons.
+  .control-group {
+    display: flex;
+    flex-wrap: wrap;
+    // Left-align controls on mobile.
+    justify-content: flex-start;
+
+    // Right-align controls on larger screens.
+    @include media-breakpoint-up(md) {
+      justify-content: flex-end;
+    }
+
+    > * {
+      // Pad each control button.
+      margin: map.get($spacers, 1);
+
+      &:first-child {
+        // Don't pad the left side of the first control button.
+        margin-left: 0;
+      }
+
+      &:last-child {
+        // Don't pad the right side of the last control button.
+        margin-right: 0;
+      }
+    }
+  }
+}
+
+.object-subtitle {
+  display: block;
+  font-size: $font-size-sm;
+  color: $text-muted;
+
+  @include media-breakpoint-up(md) {
+    display: inline-block;
+  }
+
+  > span {
+    display: block;
+
+    // Hide the separator on small screens.
+    &.separator {
+      display: none;
+    }
+
+    &,
+    &.separator {
+      @include media-breakpoint-up(md) {
+        display: inline-block;
+      }
+    }
+  }
+}
+
+// Global Search
 nav.search {
   // Don't overtake dropdowns
   z-index: 999;
   justify-content: center;
   background-color: var(--nbx-body-bg);
 
-  form button.dropdown-toggle {
-    font-weight: $input-group-addon-font-weight;
-    line-height: $input-line-height;
-    color: $input-group-addon-color;
-    background-color: $input-group-addon-bg;
-    border: $input-border-width solid $input-group-addon-border-color;
-    border-color: $input-border-color;
-    @include border-radius($input-border-radius);
-    border-left: 1px solid var(--nbx-search-filter-border-left-color);
+  .search-container {
+    display: flex;
+    width: 100%;
+
+    @include media-breakpoint-down(lg) {
+      display: none;
+    }
+  }
+
+  // Search Input & Selected Object Value & Object Selector
+  .input-group {
+    // Selected Object
+    .search-obj-selected {
+      border-color: $input-border-color;
+    }
+
+    // Object Selector Dropdown Button
+    .dropdown-toggle {
+      // Generate the same styles as a regular Bootstrap button.
+      @include button-variant($input-group-addon-bg, $input-border-color);
+      margin-left: 0;
+      font-weight: $input-group-addon-font-weight;
+      line-height: $input-line-height;
+      color: $input-group-addon-color;
+      background-color: $input-group-addon-bg;
+      border: $input-border-width solid $input-border-color;
+      @include border-radius($input-border-radius);
+      border-left: 1px solid var(--nbx-search-filter-border-left-color);
+
+      &:focus {
+        box-shadow: unset !important;
+      }
+      // Don't show the dropdown icon — the filter icon is basically the same thing.
+      &:after {
+        display: none;
+      }
+    }
+
+    // Object Selector Dropdown Menu
+    .search-obj-selector {
+      @include media-breakpoint-down(lg) {
+        // Limit the height and enable scrolling on mobile devices.
+        max-height: 70vh;
+        overflow-y: auto;
+      }
 
-    &:focus {
-      box-shadow: unset !important;
+      .dropdown-item,
+      .dropdown-header {
+        font-size: $font-size-sm;
+      }
+
+      .dropdown-header {
+        text-transform: uppercase;
+      }
     }
   }
 }
@@ -431,23 +529,6 @@ div.content-container {
   pointer-events: none;
 }
 
-.search-obj-selector {
-  @include media-breakpoint-down(lg) {
-    // Limit the height and enable scrolling on mobile devices.
-    max-height: 75vh;
-    overflow-y: auto;
-  }
-
-  .dropdown-item,
-  .dropdown-header {
-    font-size: $font-size-sm;
-  }
-
-  .dropdown-header {
-    text-transform: uppercase;
-  }
-}
-
 span.color-label {
   display: block;
   width: 5rem;
@@ -479,6 +560,14 @@ span.color-label {
   .card-body.small .form-select {
     font-size: $input-font-size-sm;
   }
+
+  .card-divider {
+    width: 100%;
+    height: 1px;
+    margin: $hr-margin-y 0;
+    border-top: 1px solid $card-border-color;
+    opacity: $hr-opacity;
+  }
 }
 
 .form-floating {

+ 8 - 0
netbox/project-static/styles/theme-dark.scss

@@ -21,6 +21,14 @@ $theme-colors: (
   'danger': $danger,
   'light': $light,
   'dark': $dark,
+  'red': $red-300,
+  'yellow': $yellow-300,
+  'green': $green-300,
+  'blue': $blue-300,
+  'cyan': $cyan-300,
+  'indigo': $indigo-300,
+  'purple': $purple-300,
+  'pink': $pink-300,
 );
 
 $theme-colors: map-merge($theme-colors, $theme-color-addons);

+ 14 - 0
netbox/project-static/styles/theme-light.scss

@@ -4,6 +4,20 @@
 
 $input-border-color: $gray-200;
 
+$theme-colors: map-merge(
+  $theme-colors,
+  (
+    'red': $red-500,
+    'yellow': $yellow-500,
+    'green': $green-500,
+    'blue': $blue-500,
+    'cyan': $cyan-500,
+    'indigo': $indigo-500,
+    'purple': $purple-500,
+    'pink': $pink-500,
+  )
+);
+
 $theme-colors: map-merge($theme-colors, $theme-color-addons);
 
 $light: $gray-200;

+ 14 - 14
netbox/templates/circuits/inc/circuit_termination.html

@@ -2,14 +2,15 @@
 
 <div class="card">
     <div class="card-header">
-        <div class="float-end">
+        <strong class="d-block d-md-inline mb-3 mb-md-0">Termination - {{ side }} Side</strong>
+        <div class="float-md-end">
             {% if not termination and perms.circuits.add_circuittermination %}
                 <a href="{% url 'circuits:circuittermination_add' circuit=object.pk %}?term_side={{ side }}" class="btn btn-sm btn-success lh-1">
                     <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add
                 </a>
             {% endif %}
             {% if termination and perms.circuits.change_circuittermination %}
-                <a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}" class="btn btn-sm btn-yellow-500 lh-1">
+                <a href="{% url 'circuits:circuittermination_edit' pk=termination.pk %}" class="btn btn-sm btn-yellow lh-1">
                     <span class="mdi mdi-pencil" aria-hidden="true"></span> Edit
                 </a>
                 <a href="{% url 'circuits:circuit_terminations_swap' pk=object.pk %}" class="btn btn-sm btn-primary lh-1">
@@ -22,7 +23,6 @@
                 </a>
             {% endif %}
         </div>
-        <strong>Termination - {{ side }} Side</strong>
     </div>
     <div class="card-body">
       {% if termination %}
@@ -44,17 +44,7 @@
                   <span class="text-success"><i class="mdi mdi-check-bold"></i></span>
                   <span class="text-muted">Marked as connected</span>
                 {% elif termination.cable %}
-                  <div class="float-end">
-                    <a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
-                      <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
-                    </a>
-                    {% if perms.dcim.delete_cable %}
-                      <a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-sm lh-1">
-                        <i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> Disconnect
-                      </a>
-                    {% endif %}
-                  </div>
-                  <a href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
+                  <a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
                   {% with peer=termination.get_cable_peer %}
                     to
                     {% if peer.device %}
@@ -64,6 +54,16 @@
                     {% endif %}
                     <a href="{{ peer.get_absolute_url }}">{{ peer }}</a>
                   {% endwith %}
+                  <div class="float-md-end mt-3 mt-md-0">
+                    <a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
+                      <i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i>
+                    </a>
+                    {% if perms.dcim.delete_cable %}
+                      <a href="{% url 'dcim:cable_delete' pk=termination.cable.pk %}?return_url={{ termination.circuit.get_absolute_url }}" title="Remove cable" class="btn btn-danger btn-sm lh-1">
+                        <i class="mdi mdi-ethernet-cable-off" aria-hidden="true"></i> Disconnect
+                      </a>
+                    {% endif %}
+                  </div>
                 {% elif perms.dcim.add_cable %}
                   <div class="dropdown">
                     <button type="button" class="btn btn-success btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

+ 167 - 193
netbox/templates/dcim/device/base.html

@@ -14,209 +14,183 @@
 {% endblock %}
 
 {% block extra_controls %}
-{% if perms.dcim.change_device %}
-<div class="dropdown m-1">
-    <button id="add-device-components" type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
-        <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components
-    </button>
-    <ul class="dropdown-menu" aria-labeled-by="add-device-components">
-    {% if perms.dcim.add_consoleport %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}"
-            >
-                Console Ports
-            </a>
-        </li>
-    {% endif %}
-    {% if perms.dcim.add_consoleserverport %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">
-                Console Server Ports
-            </a>
-        </li>
-    {% endif %}
-    {% if perms.dcim.add_powerport %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">
-                Power Ports
-            </a>
-        </li>
-    {% endif %}
-    {% if perms.dcim.add_poweroutlet %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">
-                Power Outlets
-            </a>
-        </li>
-    {% endif %}
-    {% if perms.dcim.add_interface %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">
-                Interfaces
-            </a>
-        </li>
-    {% endif %}
-    {% if perms.dcim.add_frontport %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">
-                Front Ports
-            </a>
-        </li>
-    {% endif %}
-    {% if perms.dcim.add_rearport %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">
-                Rear Ports
-            </a>
-        </li>
-    {% endif %}
-    {% if perms.dcim.add_devicebay %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}">
-                Device Bays
-            </a>
-        </li>
+    {% if perms.dcim.change_device %}
+        <div class="dropdown">
+            <button id="add-device-components" type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
+                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components
+            </button>
+            <ul class="dropdown-menu" aria-labeled-by="add-device-components">
+                {% if perms.dcim.add_consoleport %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">
+                            Console Ports
+                        </a>
+                    </li>
+                {% endif %}
+                {% if perms.dcim.add_consoleserverport %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">
+                            Console Server Ports
+                        </a>
+                    </li>
+                {% endif %}
+                {% if perms.dcim.add_powerport %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}">
+                            Power Ports
+                        </a>
+                    </li>
+                {% endif %}
+                {% if perms.dcim.add_poweroutlet %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}">
+                            Power Outlets
+                        </a>
+                    </li>
+                {% endif %}
+                {% if perms.dcim.add_interface %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">
+                            Interfaces
+                        </a>
+                    </li>
+                {% endif %}
+                {% if perms.dcim.add_frontport %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}">
+                            Front Ports
+                        </a>
+                    </li>
+                {% endif %}
+                {% if perms.dcim.add_rearport %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}">
+                            Rear Ports
+                        </a>
+                    </li>
+                {% endif %}
+                {% if perms.dcim.add_devicebay %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_devicebays' pk=object.pk %}">
+                            Device Bays
+                        </a>
+                    </li>
+                {% endif %}
+                {% if perms.dcim.add_inventoryitem %}
+                    <li>
+                        <a class="dropdown-item" href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}">
+                            Inventory Items
+                        </a>
+                    </li>
+                {% endif %}
+            </ul>
+        </div>
     {% endif %}
-    {% if perms.dcim.add_inventoryitem %}
-        <li>
-            <a
-                class="dropdown-item"
-                href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}">
-                Inventory Items
-            </a>
-        </li>
-    {% endif %}
-    </ul>
-</div>
-{% endif %}
 {% endblock %}
 
 {% block tab_items %}
-<li 
-    role="presentation"
-    class="nav-item">
-    <a
-        href="{% url 'dcim:device' pk=object.pk %}"
-        class="nav-link{% if active_tab == 'device' %} active{% endif %}"
-    >
-        Device
-    </a>
-</li>
-{% with interface_count=object.interfaces_count %}
-{% if interface_count %}
-<li role="presentation" class="nav-item">
-    <a class="nav-link {% if active_tab == 'interfaces' %} active{% endif %}" href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
-</li>
-{% endif %}
-{% endwith %}
+    <li role="presentation" class="nav-item">
+        <a href="{% url 'dcim:device' pk=object.pk %}" class="nav-link{% if active_tab == 'device' %} active{% endif %}">
+            Device
+        </a>
+    </li>
 
-{% with frontport_count=object.frontports.count %}
-{% if frontport_count %}
-<li role="presentation" class="nav-item">
-    <a class="nav-link {% if active_tab == 'front-ports' %} active{% endif %}" href="{% url 'dcim:device_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
-</li>
-{% endif %}
-{% endwith %}
+    {% with interface_count=object.interfaces_count %}
+        {% if interface_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'interfaces' %} active{% endif %}" href="{% url 'dcim:device_interfaces' pk=object.pk %}">Interfaces {% badge interface_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
 
-{% with rearport_count=object.rearports.count %}
-{% if rearport_count %}
-<li role="presentation" class="nav-item">
-    <a class="nav-link {% if active_tab == 'rear-ports' %} active{% endif %}" href="{% url 'dcim:device_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
-</li>
-{% endif %}
-{% endwith %}
+    {% with frontport_count=object.frontports.count %}
+        {% if frontport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'front-ports' %} active{% endif %}" href="{% url 'dcim:device_frontports' pk=object.pk %}">Front Ports {% badge frontport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
 
-{% with consoleport_count=object.consoleports.count %}
-{% if consoleport_count %}
-<li role="presentation" class="nav-item">
-    <a class="nav-link {% if active_tab == 'console-ports' %} active{% endif %}" href="{% url 'dcim:device_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
-</li>
-{% endif %}
-{% endwith %}
+    {% with rearport_count=object.rearports.count %}
+        {% if rearport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'rear-ports' %} active{% endif %}" href="{% url 'dcim:device_rearports' pk=object.pk %}">Rear Ports {% badge rearport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
 
-{% with consoleserverport_count=object.consoleserverports.count %}
-{% if consoleserverport_count %}
-    <li role="presentation" class="nav-item">
-        <a class="nav-link {% if active_tab == 'console-server-ports' %} active{% endif %}" href="{% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
-    </li>
-{% endif %}
-{% endwith %}
+    {% with consoleport_count=object.consoleports.count %}
+        {% if consoleport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'console-ports' %} active{% endif %}" href="{% url 'dcim:device_consoleports' pk=object.pk %}">Console Ports {% badge consoleport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
 
-{% with powerport_count=object.powerports.count %}
-{% if powerport_count %}
-<li role="presentation" class="nav-item">
-    <a class="nav-link {% if active_tab == 'power-ports' %} active{% endif %}" href="{% url 'dcim:device_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
-</li>
-{% endif %}
-{% endwith %}
+    {% with consoleserverport_count=object.consoleserverports.count %}
+        {% if consoleserverport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'console-server-ports' %} active{% endif %}" href="{% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Server Ports {% badge consoleserverport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
 
-{% with poweroutlet_count=object.poweroutlets.count %}
-{% if poweroutlet_count %}
-<li role="presentation" class="nav-item">
-    <a class="nav-link {% if active_tab == 'power-outlets' %} active{% endif %}" href="{% url 'dcim:device_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
-</li>
-{% endif %}
-{% endwith %}
+    {% with powerport_count=object.powerports.count %}
+        {% if powerport_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'power-ports' %} active{% endif %}" href="{% url 'dcim:device_powerports' pk=object.pk %}">Power Ports {% badge powerport_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
 
-{% with devicebay_count=object.devicebays.count %}
-{% if devicebay_count %}
-<li role="presentation" class="nav-item">
-    <a class="nav-link {% if active_tab == 'device-bays' %} active{% endif %}" href="{% url 'dcim:device_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
-</li>
-{% endif %}
-{% endwith %}
+    {% with poweroutlet_count=object.poweroutlets.count %}
+        {% if poweroutlet_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'power-outlets' %} active{% endif %}" href="{% url 'dcim:device_poweroutlets' pk=object.pk %}">Power Outlets {% badge poweroutlet_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
 
-{% with inventoryitem_count=object.inventoryitems.count %}
-{% if inventoryitem_count %}
-<li role="presentation" class="nav-item">
-    <a class="nav-link {% if active_tab == 'inventory' %} active{% endif %}" href="{% url 'dcim:device_inventory' pk=object.pk %}">Inventory {% badge inventoryitem_count %}</a>
-</li>
-{% endif %}
-{% endwith %}
-{% if perms.dcim.napalm_read_device and object.status == 'active' and object.primary_ip and object.platform.napalm_driver %}
-{# NAPALM-enabled tabs #}
-<li role="presentation" class="nav-item">
-    <a class="nav-link{% if active_tab == 'status' %} active{% endif %}" href="{% url 'dcim:device_status' pk=object.pk %}">
-        Status
-    </a>
-</li>
-<li role="presentation" class="nav-item">
-    <a class="nav-link{% if active_tab == 'lldp-neighbors' %} active{% endif %}" href="{% url 'dcim:device_lldp_neighbors' pk=object.pk %}">
-        LLDP Neighbors
-    </a>
-</li>
-<li role="presentation" class="nav-item">
-    <a class="nav-link{% if active_tab == 'config' %} active{% endif %}" href="{% url 'dcim:device_config' pk=object.pk %}">
-        Configuration
-    </a>
-</li>
-{% endif %}
-{% if perms.extras.view_configcontext %}
-    <li 
-        role="presentation"
-        class="nav-item">
-        <a
-            href="{% url 'dcim:device_configcontext' pk=object.pk %}"
-            class="nav-link{% if active_tab == 'config-context' %} active{% endif %}"
-        >
-            Config Context
-        </a>
-    </li>
-{% endif %}
+    {% with devicebay_count=object.devicebays.count %}
+        {% if devicebay_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'device-bays' %} active{% endif %}" href="{% url 'dcim:device_devicebays' pk=object.pk %}">Device Bays {% badge devicebay_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% with inventoryitem_count=object.inventoryitems.count %}
+        {% if inventoryitem_count %}
+            <li role="presentation" class="nav-item">
+                <a class="nav-link {% if active_tab == 'inventory' %} active{% endif %}" href="{% url 'dcim:device_inventory' pk=object.pk %}">Inventory {% badge inventoryitem_count %}</a>
+            </li>
+        {% endif %}
+    {% endwith %}
+
+    {% if perms.dcim.napalm_read_device and object.status == 'active' and object.primary_ip and object.platform.napalm_driver %}
+        {# NAPALM-enabled tabs #}
+        <li role="presentation" class="nav-item">
+            <a class="nav-link{% if active_tab == 'status' %} active{% endif %}" href="{% url 'dcim:device_status' pk=object.pk %}">
+                Status
+            </a>
+        </li>
+        <li role="presentation" class="nav-item">
+            <a class="nav-link{% if active_tab == 'lldp-neighbors' %} active{% endif %}" href="{% url 'dcim:device_lldp_neighbors' pk=object.pk %}">
+                LLDP Neighbors
+            </a>
+        </li>
+        <li role="presentation" class="nav-item">
+            <a class="nav-link{% if active_tab == 'config' %} active{% endif %}" href="{% url 'dcim:device_config' pk=object.pk %}">
+                Configuration
+            </a>
+        </li>
+    {% endif %}
+    
+    {% if perms.extras.view_configcontext %}
+        <li role="presentation" class="nav-item">
+            <a href="{% url 'dcim:device_configcontext' pk=object.pk %}" class="nav-link{% if active_tab == 'config-context' %} active{% endif %}">
+                Config Context
+            </a>
+        </li>
+    {% endif %}
 {% endblock %}

+ 1 - 1
netbox/templates/dcim/devicetype.html

@@ -12,7 +12,7 @@
 
 {% block extra_controls %}
   {% if perms.dcim.change_devicetype %}
-    <div class="dropdown m-1">
+    <div class="dropdown">
       <button type="button" class="btn btn-primary btn-sm dropdown-toggle"data-bs-toggle="dropdown" aria-expanded="false">
         <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Components
       </button>

+ 1 - 1
netbox/templates/dcim/interface.html

@@ -5,7 +5,7 @@
 
 {% block extra_controls %}
   {% if perms.dcim.add_interface and not object.is_virtual %}
-    <a href="{% url 'dcim:interface_add' %}?device={{ object.device.pk }}&parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-success m-1">
+    <a href="{% url 'dcim:interface_add' %}?device={{ object.device.pk }}&parent={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-success">
       <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Child Interface
     </a>
   {% endif %}

+ 3 - 3
netbox/templates/dcim/rack.html

@@ -18,14 +18,14 @@
 {% endblock %}
 
 {% block extra_controls %}
-  <button class="btn btn-sm btn-outline-blue-500 m-1 toggle-images" selected="selected">
+  <button class="btn btn-sm btn-outline-primary toggle-images" selected="selected">
     <i class="mdi mdi-file-image-outline"></i> 
     Hide Images
   </button>
-  <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary m-1{% if not prev_rack %} disabled{% endif %}">
+  <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not prev_rack %} disabled{% endif %}">
     <i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous
   </a>
-  <a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}{% endif %}" class="btn btn-sm btn-primary m-1{% if not next_rack %} disabled{% endif %}">
+  <a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}{% endif %}" class="btn btn-sm btn-primary{% if not next_rack %} disabled{% endif %}">
     <i class="mdi mdi-chevron-right" aria-hidden="true"></i> Next
   </a>
 {% endblock %}

+ 5 - 5
netbox/templates/dcim/rack_elevation_list.html

@@ -5,16 +5,16 @@
 {% block title %}Rack Elevations{% endblock %}
 
 {% block controls %}
-    <div class="container mb-2 mx-0">
-        <div class="d-flex flex-wrap justify-content-end">
-            <button class="btn btn-sm btn-outline-dark toggle-images m-1" selected="selected">
+    <div class="controls">
+        <div class="control-group">
+            <button class="btn btn-sm btn-outline-dark toggle-images" selected="selected">
                 <span class="mdi mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Images
             </button>
-            <div class="btn-group btn-group-sm m-1" role="group">
+            <div class="btn-group btn-group-sm" role="group">
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-outline-secondary{% if rack_face == 'front' %} active{% endif %}">Front</a>
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-outline-secondary{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
             </div>
-            <div class="btn-group btn-group-sm m-1" role="group">
+            <div class="btn-group btn-group-sm" role="group">
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse=None %}" class="btn btn-outline-secondary{% if not reverse %} active{% endif %}">Normal</a>
                 <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-outline-secondary{% if reverse %} active{% endif %}">Reversed</a>
             </div>

+ 11 - 9
netbox/templates/generic/object.html

@@ -20,20 +20,19 @@
 {% block title %}{{ object }}{% endblock %}
 
 {% block subtitle %}
-  <small class="text-muted">
-    Created {{ object.created|annotated_date }} &middot;
-    Updated <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> ago
-    <span class="badge bg-secondary">{{ object|meta:"app_label" }}.{{ object|meta:"model_name" }}:{{ object.pk }}</span>
-  </small>
+  <div class="object-subtitle">
+    <span>Created {{ object.created|annotated_date }}</span>
+    <span class="separator">&middot;</span>
+    <span>Updated <span title="{{ object.last_updated }}">{{ object.last_updated|timesince }}</span> ago</span>
+  </div>
 {% endblock %}
 
 {% block controls %}
   {# Clone/Edit/Delete Buttons #}
-  <div class="controls pb-2 mx-0">
-    <div class="d-flex flex-wrap justify-content-end mb-2">
-      {% custom_links object %}
+  <div class="controls">
+    <div class="control-group">
       {% plugin_buttons object %}
-  
+
       {# Extra buttons #}
       {% block extra_controls %}{% endblock %}
 
@@ -48,6 +47,9 @@
       {% endif %}
 
     </div>
+    <div class="control-group">
+      {% custom_links object %}
+    </div>
   </div>
 {% endblock controls %}
 

+ 6 - 4
netbox/templates/generic/object_edit.html

@@ -8,10 +8,12 @@
 
 {% block controls %}
   {% if settings.DOCS_ROOT %}
-    <div class="controls pt-1">
-      <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#docs_modal" title="Help">
-        <i class="mdi mdi-help-circle"></i> Help
-      </button>
+    <div class="controls">
+      <div class="control-group">
+        <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#docs_modal" title="Help">
+          <i class="mdi mdi-help-circle"></i> Help
+        </button>
+      </div>
     </div>
   {% endif %}
 {% endblock controls %}

+ 2 - 2
netbox/templates/generic/object_list.html

@@ -7,8 +7,8 @@
 {% block title %}{{ content_type.model_class|meta:"verbose_name_plural"|bettertitle }}{% endblock %}
 
 {% block controls %}
-<div class="controls mb-2 mx-0">
-  <div class="d-flex flex-wrap justify-content-end">
+<div class="controls">
+  <div class="control-group">
     {% block extra_controls %}{% endblock %}
     {% if permissions.add and 'add' in action_buttons %}
         {% add_button content_type.model_class|validated_viewname:"add" %}

+ 6 - 3
netbox/templates/inc/filter_list.html

@@ -22,7 +22,7 @@
                                                 {{ field }}
                                             </div>
                                         {% else %}
-                                            <div class="mb-3 mx-3">
+                                            <div class="mb-3 px-2">
                                                 <label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
                                                 {{ field }}
                                             </div>
@@ -30,17 +30,20 @@
                                     {% endwith %}
                                 {% endfor %}
                             </div>
+                            {% if forloop.counter != filter_form.field_groups|length %}
+                                <hr class="card-divider mt-0" />
+                            {% endif %}
                         {% endfor %}
                     {% else %}
                         {% for field in filter_form.visible_fields %}
-                            <div class="col">
+                            <div class="col col-12">
                                 {% if field|widget_type == 'checkboxinput' %}
                                     <div class="form-check mb-3">
                                         <label class="form-check-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
                                         {{ field }}
                                     </div>
                                 {% else %}
-                                    <div class="mb-3">
+                                    <div class="mb-3 px-2">
                                     <label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
                                     {{ field }}
                                     </div>

+ 4 - 3
netbox/templates/ipam/inc/toggle_available.html

@@ -1,10 +1,11 @@
 {% load helpers %}
+
 {% if show_available is not None %}
-  <div class="btn-group m-1" role="group">
-    <a href="{{ request.path }}{% querystring request show_available='true' %}" class="btn btn-sm btn-outline-blue-500{% if show_available %} active disabled{% endif %}">
+  <div class="btn-group" role="group">
+    <a href="{{ request.path }}{% querystring request show_available='true' %}" class="btn btn-sm btn-outline-primary{% if show_available %} active disabled{% endif %}">
       <i class="mdi mdi-eye"></i> Show Available
     </a>
-    <a href="{{ request.path }}{% querystring request show_available='false' %}" class="btn btn-sm btn-outline-blue-500{% if not show_available %} active disabled{% endif %}">
+    <a href="{{ request.path }}{% querystring request show_available='false' %}" class="btn btn-sm btn-outline-primary{% if not show_available %} active disabled{% endif %}">
       <i class="mdi mdi-eye-off"></i> Hide Available
     </a>
   </div>

+ 1 - 1
netbox/templates/ipam/iprange/ip_addresses.html

@@ -2,7 +2,7 @@
 
 {% block extra_controls %}
   {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and object.first_available_ip %}
-    <a href="{% url 'ipam:ipaddress_add' %}?address={{ object.first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-primary m-1">
+    <a href="{% url 'ipam:ipaddress_add' %}?address={{ object.first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-primary">
         <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Address
     </a>
   {% endif %}

+ 1 - 1
netbox/templates/ipam/prefix/ip_addresses.html

@@ -2,7 +2,7 @@
 
 {% block extra_controls %}
   {% if perms.ipam.add_ipaddress and active_tab == 'ip-addresses' and first_available_ip %}
-    <a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-primary m-1">
+    <a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}&vrf={{ object.vrf.pk }}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}" class="btn btn-sm btn-primary">
         <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add IP Address
     </a>
   {% endif %}

+ 2 - 2
netbox/templates/ipam/prefix_list.html

@@ -2,7 +2,7 @@
 {% load helpers %}
 
 {% block extra_controls %}
-    <div class="dropdown m-1">
+    <div class="dropdown">
         <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="max_depth" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
             Max Depth{% if "depth__lte" in request.GET %}: {{ request.GET.depth__lte }}{% endif %}
         </button>
@@ -19,7 +19,7 @@
             {% endfor %}
         </ul>
     </div>
-    <div class="dropdown m-1">
+    <div class="dropdown">
         <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="max_length" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
             Max Length{% if "mask_length__lte" in request.GET %}: {{ request.GET.mask_length__lte }}{% endif %}
         </button>

+ 20 - 19
netbox/templates/virtualization/cluster/base.html

@@ -13,35 +13,36 @@
 
 {% block extra_controls %}
   {% if perms.virtualization.change_cluster and perms.virtualization.add_virtualmachine %}
-    <a href="{% url 'virtualization:virtualmachine_add' %}?cluster_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary m-1">
+    <a href="{% url 'virtualization:virtualmachine_add' %}?cluster_id={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-sm btn-primary">
       <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Virtual Machine
     </a>
   {% endif %}
+
   {% if perms.virtualization.change_cluster %}
-    <a href="{% url 'virtualization:cluster_add_devices' pk=object.pk %}?site_id={{ object.site.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm m-1">
+    <a href="{% url 'virtualization:cluster_add_devices' pk=object.pk %}?site_id={{ object.site.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
       <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Assign Device
     </a>
   {% endif %}
 {% endblock %}
 
 {% block tab_items %}
-<li role="presentation" class="nav-item">
-  <a href="{{ object.get_absolute_url }}" class="nav-link{% if not active_tab %} active{% endif %}">Cluster</a>
-</li>
+  <li role="presentation" class="nav-item">
+    <a href="{{ object.get_absolute_url }}" class="nav-link{% if not active_tab %} active{% endif %}">Cluster</a>
+  </li>
 
-{% with virtualmachine_count=object.virtual_machines.count %}
-<li role="presentation" class="nav-item">
-  <a href="{% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="nav-link{% if active_tab == 'virtual-machines' %} active{% endif %}">
-    Virtual Machines {% badge virtualmachine_count %}
-  </a>
-</li>
-{% endwith %}
+  {% with virtualmachine_count=object.virtual_machines.count %}
+    <li role="presentation" class="nav-item">
+      <a href="{% url 'virtualization:cluster_virtualmachines' pk=object.pk %}" class="nav-link{% if active_tab == 'virtual-machines' %} active{% endif %}">
+        Virtual Machines {% badge virtualmachine_count %}
+      </a>
+    </li>
+  {% endwith %}
 
-{% with device_count=object.devices.count %}
-<li role="presentation" class="nav-item">
-  <a href="{% url 'virtualization:cluster_devices' pk=object.pk %}" class="nav-link{% if active_tab == 'devices' %} active{% endif %}">
-    Devices {% badge device_count %}
-  </a>
-</li>
-{% endwith %}
+  {% with device_count=object.devices.count %}
+    <li role="presentation" class="nav-item">
+      <a href="{% url 'virtualization:cluster_devices' pk=object.pk %}" class="nav-link{% if active_tab == 'devices' %} active{% endif %}">
+        Devices {% badge device_count %}
+      </a>
+    </li>
+  {% endwith %}
 {% endblock %}

+ 2 - 2
netbox/templates/virtualization/virtualmachine/base.html

@@ -10,8 +10,8 @@
 
 {% block extra_controls %}
   {% if perms.virtualization.add_vminterface %}
-    <a href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-sm btn-primary m-1">
-      <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Interfaces
+    <a href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}" class="btn btn-sm btn-primary">
+      <i class="mdi mdi-plus-thick"></i> Add Interfaces
     </a>
   {% endif %}
 {% endblock %}

+ 6 - 0
netbox/tenancy/forms.py

@@ -64,6 +64,11 @@ class TenantGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
 
 class TenantGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = TenantGroup
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     parent_id = DynamicModelMultipleChoiceField(
         queryset=TenantGroup.objects.all(),
         required=False,
@@ -132,6 +137,7 @@ class TenantFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
     model = Tenant
     q = forms.CharField(
         required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
         label=_('Search')
     )
     group_id = DynamicModelMultipleChoiceField(

+ 5 - 2
netbox/utilities/templates/buttons/add.html

@@ -1,6 +1,9 @@
-<div class="d-flex flex-shrink-1 m-1">
+{% comment %} <div class="d-flex flex-shrink-1">
   <a href="{{ add_url }}" type="button" class="btn btn-sm btn-success">
     <i class="mdi mdi-plus-thick"></i>
     &nbsp;Add
   </a>
-</div>
+</div> {% endcomment %}
+<a href="{{ add_url }}" type="button" class="btn btn-sm btn-success">
+  <i class="mdi mdi-plus-thick"></i> Add
+</a>

+ 1 - 1
netbox/utilities/templates/buttons/clone.html

@@ -1,3 +1,3 @@
-<a href="{{ url }}" class="btn btn-sm btn-success m-1" role="button">
+<a href="{{ url }}" class="btn btn-sm btn-success" role="button">
     <i class="mdi mdi-content-copy" aria-hidden="true"></i>&nbsp;Clone
 </a>

+ 1 - 1
netbox/utilities/templates/buttons/delete.html

@@ -1,3 +1,3 @@
-<a href="{{ url }}" class="btn btn-sm btn-danger m-1" role="button">
+<a href="{{ url }}" class="btn btn-sm btn-danger" role="button">
     <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>&nbsp;Delete
 </a>

+ 1 - 1
netbox/utilities/templates/buttons/edit.html

@@ -1,3 +1,3 @@
-<a href="{{ url }}" class="btn btn-sm btn-warning m-1" role="button">
+<a href="{{ url }}" class="btn btn-sm btn-warning" role="button">
     <span class="mdi mdi-pencil" aria-hidden="true"></span>&nbsp;Edit
 </a>

+ 2 - 2
netbox/utilities/templates/buttons/export.html

@@ -1,5 +1,5 @@
-<div class="dropdown m-1">
-    <button type="button" class="btn btn-sm btn-purple-500 dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+<div class="dropdown">
+    <button type="button" class="btn btn-sm btn-purple dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
         <i class="mdi mdi-download"></i>&nbsp;Export
     </button>
     <ul class="dropdown-menu dropdown-menu-end">

+ 1 - 1
netbox/utilities/templates/buttons/import.html

@@ -1,3 +1,3 @@
-<a href="{% url import_url %}" type="button" class="btn btn-sm btn-info m-1">
+<a href="{% url import_url %}" type="button" class="btn btn-sm btn-info">
   <i class="mdi mdi-upload"></i>&nbsp;Import
 </a>

+ 1 - 1
netbox/utilities/templates/search/searchbar.html

@@ -12,7 +12,7 @@
 
   <span class="input-group-text search-obj-selected">All Objects</span>
 
-  <button type="button" aria-expanded="false" data-bs-toggle="dropdown" class="btn btn-outline-secondary dropdown-toggle">
+  <button type="button" aria-expanded="false" data-bs-toggle="dropdown" class="btn dropdown-toggle">
     <i class="mdi mdi-filter-variant"></i>
   </button>
 

+ 19 - 1
netbox/virtualization/forms.py

@@ -225,14 +225,20 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBul
 class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
     model = Cluster
     field_order = [
-        'type_id', 'region_id', 'site_id', 'group_id', 'tenant_group_id', 'tenant_id',
+        'q', 'type_id', 'region_id', 'site_id', 'group_id', 'tenant_group_id', 'tenant_id',
     ]
     field_groups = [
+        ['q'],
         ['type_id'],
         ['region_id', 'site_id'],
         ['tenant_group_id', 'tenant_id'],
         ['tag'],
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     type_id = DynamicModelMultipleChoiceField(
         queryset=ClusterType.objects.all(),
         required=False,
@@ -540,6 +546,7 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
         'site_id', 'tenant_group_id', 'tenant_id', 'platform_id', 'mac_address',
     ]
     field_groups = [
+        ['q'],
         ['status', 'role_id'],
         ['platform_id', 'mac_address'],
         ['cluster_group_id', 'cluster_type_id', 'cluster_id'],
@@ -547,6 +554,11 @@ class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldMod
         ['tenant_group_id', 'tenant_id'],
 
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     cluster_group_id = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         required=False,
@@ -855,10 +867,16 @@ class VMInterfaceBulkRenameForm(BulkRenameForm):
 class VMInterfaceFilterForm(BootstrapMixin, forms.Form):
     model = VMInterface
     field_groups = [
+        ['q'],
         ['cluster_id', 'virtual_machine_id'],
         ['enabled', 'mac_address'],
         ['tag']
     ]
+    q = forms.CharField(
+        required=False,
+        widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
+        label=_('Search')
+    )
     cluster_id = DynamicModelMultipleChoiceField(
         queryset=Cluster.objects.all(),
         required=False,

Некоторые файлы не были показаны из-за большого количества измененных файлов