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

Merge branch 'feature' into 6000-cable-trace-svg

jeremystretch 4 лет назад
Родитель
Сommit
145be09cfd
51 измененных файлов с 464 добавлено и 320 удалено
  1. 1 1
      netbox/project-static/README.md
  2. 5 5
      netbox/project-static/bundle.js
  3. 1 1
      netbox/project-static/dist/cable_trace.css.map
  4. 0 0
      netbox/project-static/dist/config.js
  5. 0 0
      netbox/project-static/dist/config.js.map
  6. 0 0
      netbox/project-static/dist/jobs.js
  7. 0 0
      netbox/project-static/dist/jobs.js.map
  8. 0 0
      netbox/project-static/dist/lldp.js
  9. 0 0
      netbox/project-static/dist/lldp.js.map
  10. 0 0
      netbox/project-static/dist/netbox-dark.css
  11. 0 0
      netbox/project-static/dist/netbox-dark.css.map
  12. 0 0
      netbox/project-static/dist/netbox-external.css
  13. 0 0
      netbox/project-static/dist/netbox-external.css.map
  14. 0 0
      netbox/project-static/dist/netbox-light.css
  15. 0 0
      netbox/project-static/dist/netbox-light.css.map
  16. 0 0
      netbox/project-static/dist/netbox.js
  17. 0 0
      netbox/project-static/dist/netbox.js.map
  18. 0 0
      netbox/project-static/dist/rack_elevation.css.map
  19. 0 0
      netbox/project-static/dist/status.js
  20. 0 0
      netbox/project-static/dist/status.js.map
  21. 29 1
      netbox/project-static/src/bs.ts
  22. 15 0
      netbox/project-static/src/links.ts
  23. 2 0
      netbox/project-static/src/netbox.ts
  24. 0 0
      netbox/project-static/styles/_cable_trace.scss
  25. 0 0
      netbox/project-static/styles/_dark.scss
  26. 0 0
      netbox/project-static/styles/_elevations.scss
  27. 0 0
      netbox/project-static/styles/_external.scss
  28. 0 0
      netbox/project-static/styles/_light.scss
  29. 0 0
      netbox/project-static/styles/bootstrap.scss
  30. 0 0
      netbox/project-static/styles/flatpickr-dark.scss
  31. 109 5
      netbox/project-static/styles/netbox.scss
  32. 0 0
      netbox/project-static/styles/select.scss
  33. 1 0
      netbox/project-static/styles/theme-base.scss
  34. 1 0
      netbox/project-static/styles/theme-dark.scss
  35. 2 0
      netbox/project-static/styles/theme-light.scss
  36. 1 2
      netbox/templates/base/layout.html
  37. 21 24
      netbox/templates/dcim/device/consoleports.html
  38. 21 24
      netbox/templates/dcim/device/consoleserverports.html
  39. 18 21
      netbox/templates/dcim/device/devicebays.html
  40. 21 24
      netbox/templates/dcim/device/frontports.html
  41. 23 16
      netbox/templates/dcim/device/interfaces.html
  42. 18 20
      netbox/templates/dcim/device/inventory.html
  43. 21 24
      netbox/templates/dcim/device/poweroutlets.html
  44. 22 25
      netbox/templates/dcim/device/powerports.html
  45. 21 24
      netbox/templates/dcim/device/rearports.html
  46. 4 4
      netbox/templates/dcim/rack.html
  47. 0 17
      netbox/templates/extras/objectchange.html
  48. 15 33
      netbox/templates/generic/object_list.html
  49. 54 41
      netbox/templates/home.html
  50. 37 0
      netbox/templates/inc/table_controls.html
  51. 1 8
      netbox/templates/virtualization/virtualmachine/interfaces.html

+ 1 - 1
netbox/project-static/README.md

@@ -27,7 +27,7 @@ For JavaScript, every `.ts` file in `netbox/project-static/src` is:
 2. Minified
 3. Combined into a single output file at `netbox/project-static/dist/netbox.js` (this includes any dependant libraries imported in a file)
 
-Likewise, with Sass, each `*.scss` file is:
+Likewise, with Sass, every `.scss` file in  `netbox/project-static/styles` is:
 
 1. Transpiled from Sass to CSS
 2. Minified

+ 5 - 5
netbox/project-static/bundle.js

@@ -27,11 +27,11 @@ if (args.includes('--no-cache')) {
 // Style (SCSS) bundle jobs. Generally, everything should be bundled into netbox.css from main.scss
 // unless there is a specific reason to do otherwise.
 const styles = [
-  ['_external.scss', 'netbox-external.css'],
-  ['_light.scss', 'netbox-light.css'],
-  ['_dark.scss', 'netbox-dark.css'],
-  ['_elevations.scss', 'rack_elevation.css'],
-  ['_cable_trace.scss', 'cable_trace.css'],
+  ['styles/_external.scss', 'netbox-external.css'],
+  ['styles/_light.scss', 'netbox-light.css'],
+  ['styles/_dark.scss', 'netbox-dark.css'],
+  ['styles/_elevations.scss', 'rack_elevation.css'],
+  ['styles/_cable_trace.scss', 'cable_trace.css'],
 ];
 
 // Script (JavaScript) bundle jobs. Generally, everything should be bundled into netbox.js from

+ 1 - 1
netbox/project-static/dist/cable_trace.css.map

@@ -1 +1 @@
-{"version":3,"sources":["_cable_trace.scss"],"names":[],"mappings":"AAAA,EACI,sBAAA,CACA,eAEJ,KACI,kBAAA,CACA,yBAEJ,UACE,gBAKA,SACE,YAAA,CACA,cAAA,CACA,eACA,sBACE,aAKJ,oBACE,kBAEF,SACE,iBAEF,sBACE,cAAA,CACA,iBAEF,oBACE,aAAA,CACA","file":"cable_trace.css","sourceRoot":"..","sourcesContent":["* {\n    font-family: sans-serif;\n    font-size: 14px;\n}\ntext {\n    text-anchor: middle;\n    dominant-baseline: middle;\n}\ntext.bold {\n  font-weight: bold;\n}\n\nsvg {\n  /* Boxes */\n  rect {\n    fill: #e0e0e0;\n    stroke: #606060;\n    stroke-width: 1;\n    .termination {\n      fill: #f0f0f0;\n    }\n  }\n\n  /* Connectors */\n  .connector text {\n    text-anchor: start;\n  }\n  line {\n    stroke-width: 5px;\n  }\n  line.cable-shadow {\n    stroke: #303030;\n    stroke-width: 7px;\n  }\n  line.attachment {\n    stroke: #c0c0c0;\n    stroke-dasharray: 5px,5px;\n  }\n}\n"]}
+{"version":3,"sources":["_cable_trace.scss"],"names":[],"mappings":"AAAA,EACI,sBAAA,CACA,eAEJ,KACI,kBAAA,CACA,yBAEJ,UACE,gBAKA,SACE,YAAA,CACA,cAAA,CACA,eACA,sBACE,aAKJ,oBACE,kBAEF,SACE,iBAEF,sBACE,cAAA,CACA,iBAEF,oBACE,aAAA,CACA","file":"cable_trace.css","sourceRoot":"../styles","sourcesContent":["* {\n    font-family: sans-serif;\n    font-size: 14px;\n}\ntext {\n    text-anchor: middle;\n    dominant-baseline: middle;\n}\ntext.bold {\n  font-weight: bold;\n}\n\nsvg {\n  /* Boxes */\n  rect {\n    fill: #e0e0e0;\n    stroke: #606060;\n    stroke-width: 1;\n    .termination {\n      fill: #f0f0f0;\n    }\n  }\n\n  /* Connectors */\n  .connector text {\n    text-anchor: start;\n  }\n  line {\n    stroke-width: 5px;\n  }\n  line.cable-shadow {\n    stroke: #303030;\n    stroke-width: 7px;\n  }\n  line.attachment {\n    stroke: #c0c0c0;\n    stroke-dasharray: 5px,5px;\n  }\n}\n"]}

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


+ 29 - 1
netbox/project-static/src/bs.ts

@@ -117,13 +117,41 @@ function initTabs() {
   }
 }
 
+/**
+ * When accordion buttons are clicked, add a class to the parent accordion item. This is used
+ * for the side navigation to apply a box-shadow when the section is open.
+ */
+function initSidebarAccordions(): void {
+  const items = document.querySelectorAll<HTMLDivElement>('.sidebar .accordion-item');
+
+  function handleToggle(thisItem: HTMLDivElement) {
+    for (const item of items) {
+      if (item !== thisItem) {
+        // Remove the is-open class from all other accordion items, so that if one is clicked while
+        // another is open, the shadow is removed.
+        item.classList.remove('is-open');
+      } else {
+        item.classList.toggle('is-open');
+      }
+    }
+  }
+
+  for (const item of items) {
+    for (const button of item.querySelectorAll<HTMLButtonElement>('.accordion-button')) {
+      button.addEventListener('click', () => {
+        handleToggle(item);
+      });
+    }
+  }
+}
+
 /**
  * Enable any defined Bootstrap Tooltips.
  *
  * @see https://getbootstrap.com/docs/5.0/components/tooltips
  */
 export function initBootstrap(): void {
-  for (const func of [initTooltips, initModals, initMasonry, initTabs]) {
+  for (const func of [initTooltips, initModals, initMasonry, initTabs, initSidebarAccordions]) {
     func();
   }
 }

+ 15 - 0
netbox/project-static/src/links.ts

@@ -0,0 +1,15 @@
+import { isTruthy, getElements } from './util';
+
+/**
+ * Allow any element to be made "clickable" with the use of the `data-href` attribute.
+ */
+export function initLinks() {
+  for (const link of getElements('*[data-href]')) {
+    const href = link.getAttribute('data-href');
+    if (isTruthy(href)) {
+      link.addEventListener('click', () => {
+        window.location.assign(href);
+      });
+    }
+  }
+}

+ 2 - 0
netbox/project-static/src/netbox.ts

@@ -11,6 +11,7 @@ import { initTableConfig } from './tableConfig';
 import { initInterfaceTable } from './tables';
 import { initSideNav } from './sidenav';
 import { initRackElevation } from './racks';
+import { initLinks } from './links';
 
 function init() {
   for (const init of [
@@ -27,6 +28,7 @@ function init() {
     initInterfaceTable,
     initSideNav,
     initRackElevation,
+    initLinks,
   ]) {
     init();
   }

+ 0 - 0
netbox/project-static/_cable_trace.scss → netbox/project-static/styles/_cable_trace.scss


+ 0 - 0
netbox/project-static/_dark.scss → netbox/project-static/styles/_dark.scss


+ 0 - 0
netbox/project-static/_elevations.scss → netbox/project-static/styles/_elevations.scss


+ 0 - 0
netbox/project-static/_external.scss → netbox/project-static/styles/_external.scss


+ 0 - 0
netbox/project-static/_light.scss → netbox/project-static/styles/_light.scss


+ 0 - 0
netbox/project-static/bootstrap.scss → netbox/project-static/styles/bootstrap.scss


+ 0 - 0
netbox/project-static/flatpickr-dark.scss → netbox/project-static/styles/flatpickr-dark.scss


+ 109 - 5
netbox/project-static/netbox.scss → netbox/project-static/styles/netbox.scss

@@ -98,7 +98,11 @@
   margin-bottom: $spacer;
 }
 
-// Use proper contrasting color for badge & progress-bar foreground color.
+*[data-href] {
+  cursor: pointer;
+}
+
+// Use proper contrasting color foreground color for special components.
 @each $color, $value in $theme-colors {
   .badge,
   .toast,
@@ -107,6 +111,16 @@
       color: color-contrast($value);
     }
   }
+  // Use proper foreground color in the alert body. Note: this is applied to a, p, & small because
+  // we *don't* want to override the h1-h6 colors for alerts, since those are set to a color
+  // similar to the alert color.
+  .alert.alert-#{$color} {
+    a,
+    p,
+    small {
+      color: color-contrast($value);
+    }
+  }
 }
 
 // Ensure progress bars (utilization graph) in tables aren't too narrow to display the percentage.
@@ -114,6 +128,62 @@ table td > .progress {
   min-width: 6rem;
 }
 
+.card > .table.table-flush {
+  margin-bottom: 0;
+  overflow: hidden;
+  border-bottom-right-radius: $card-border-radius;
+  border-bottom-left-radius: $card-border-radius;
+  thead th[scope='col'] {
+    background-color: $table-flush-header-bg;
+    vertical-align: middle;
+    text-transform: uppercase;
+    padding-top: map.get($spacers, 3);
+    padding-bottom: map.get($spacers, 3);
+    border-bottom-color: $card-border-color;
+    border-top: 1px solid $card-border-color;
+  }
+  th,
+  td {
+    border-left: 0;
+    border-right: 0;
+    padding-left: map.get($spacers, 4) !important;
+    padding-right: map.get($spacers, 4) !important;
+  }
+  tr[class] {
+    border-color: $card-border-color !important;
+    &:last-of-type {
+      border-bottom-color: transparent !important;
+      border-bottom-right-radius: $card-border-radius;
+      border-bottom-left-radius: $card-border-radius;
+    }
+  }
+}
+
+// Primarily used for the new release notification, but could be used for other alerts as needed.
+// Wrap any alerts in .header-alert-container to ensure the layout is consistent.
+.header-alert-container {
+  // Center-align the alert(s).
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  // Apply the same spacing that's applied to the #content div's first child (.px-3).
+  padding: 0 $spacer;
+
+  // By default, alerts inside .header-alert-container should take up the full width.
+  .alert {
+    width: 100%;
+
+    // Adjust the max-width for larger screens so there's not a big ugly blue blob taking up the
+    // entire screen.
+    @include media-breakpoint-up(md) {
+      max-width: 75%;
+    }
+    @include media-breakpoint-up(lg) {
+      max-width: 50%;
+    }
+  }
+}
+
 span.profile-button .dropdown-menu {
   transition: opacity 0.2s ease-in-out;
   display: block !important;
@@ -441,6 +511,12 @@ div.content-container {
 
   div.accordion-item {
     border: unset;
+    padding: 0 $spacer / 2;
+
+    // When an sidenav section is open, apply a shadow to provide a visual border.
+    &.is-open {
+      box-shadow: inset 0px -25px 20px -25px rgba($black, 0.25);
+    }
 
     & > a.accordion-button {
       &:not(.collapsed) {
@@ -483,13 +559,13 @@ div.content-container {
     }
   }
   div.position-sticky {
-    height: calc(100vh - 8rem);
+    height: calc(100vh - #{$sidebar-bottom-height});
   }
   div.sidebar-bottom {
     padding-left: 0.5rem;
     padding-right: 0.5rem;
     position: sticky;
-    height: 8rem;
+    height: $sidebar-bottom-height;
     background-color: var(--nbx-sidebar-bg);
 
     @include media-breakpoint-down(md) {
@@ -699,10 +775,38 @@ label.required {
   }
 }
 
+// Applied to containing element around table bulk-action buttons (bulk-edit, bulk-disconnect
+// bulk-delete, etc). Each usage of .bulk-buttons needs to have groups of buttons wrapped with
+// .bulk-button-group so that proper spacing is applied (even if there is only one group).
 div.bulk-buttons {
   display: flex;
-  & > * {
-    margin: $spacer / 4;
+  justify-content: space-between;
+  margin: $spacer / 2 0;
+  // Each group of buttons needs to be contained separately for alignment purposes. This way, you
+  // can put some buttons in a group that aligns left, and other buttons in a group that aligns
+  // right.
+  & > div.bulk-button-group {
+    display: flex;
+    &:first-of-type:not(:last-of-type) {
+      // If there are multiple bulk button groups and this is the first, the first button in the
+      // group should *not* have left spacing applied, so the button group aligns with the rest
+      // of the page elements.
+      & > *:first-child {
+        margin-left: 0;
+      }
+    }
+    &:last-of-type:not(:first-of-type) {
+      // If there are multiple bulk button groups and this is the last, the last button in the
+      // group should *not* have right spacing applied, so the button group aligns with the rest
+      // of the page elements.
+      & > *:last-child {
+        margin-right: 0;
+      }
+    }
+    // However, the rest of the buttons should have spacing applied in all directions.
+    & > * {
+      margin: $spacer / 4;
+    }
   }
 }
 

+ 0 - 0
netbox/project-static/select.scss → netbox/project-static/styles/select.scss


+ 1 - 0
netbox/project-static/theme-base.scss → netbox/project-static/styles/theme-base.scss

@@ -245,3 +245,4 @@ $accordion-padding-y: 0.8125rem;
 $accordion-padding-x: 0.8125rem;
 
 $sidebar-width: 280px;
+$sidebar-bottom-height: 4rem;

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

@@ -63,6 +63,7 @@ $table-active-color: $table-color;
 $table-active-bg: rgba($white, $table-active-bg-factor);
 $table-hover-color: $table-color;
 $table-hover-bg: rgba($white, $table-hover-bg-factor);
+$table-flush-header-bg: $gray-700;
 
 // Buttons
 $btn-box-shadow: inset 0 1px 0 rgba($black, 0.15), 0 1px 1px rgba($white, 0.075);

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

@@ -28,3 +28,5 @@ $code-color: $gray-900;
 
 $list-group-color: $gray-700;
 $list-group-disabled-color: $gray-500;
+
+$table-flush-header-bg: $gray-100;

+ 1 - 2
netbox/templates/base/layout.html

@@ -21,7 +21,7 @@
             </a>
           </div>
 
-          <ul class="nav flex-column px-1">
+          <ul class="nav flex-column">
 
             {# Search bar for collapsed menu #}
             <div class="d-block d-md-none mx-1 my-3 search-container">
@@ -120,7 +120,6 @@
         {# Page footer #}
         <footer class="footer container-fluid pb-3 pt-4 px-0">
           <div class="row align-items-center justify-content-end mx-0">
-          <div class="col-auto d-none d-md-block"></div>
             <div class="col text-center small text-muted">
               <span class="fw-light d-block d-md-inline">{% annotated_now %} {% now 'T' %}</span>
               <span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>

+ 21 - 24
netbox/templates/dcim/device/consoleports.html

@@ -6,37 +6,34 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end noprint">
-      {% if request.user.is_authenticated %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DeviceConsolePortTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
-      {% endif %}
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="DeviceConsolePortTable_config" %}
     {% render_table consoleport_table 'inc/table.html' %}
-    <div class="noprint">
-        {% if perms.dcim.change_consoleport %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-        {% endif %}
-        {% if perms.dcim.delete_consoleport %}
-            <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-        {% endif %}
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
+            {% if perms.dcim.change_consoleport %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleport_bulk_rename' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
+                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                </button>
+                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
+                    <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+                </button>
+            {% endif %}
+            {% if perms.dcim.delete_consoleport %}
+                <button type="submit" name="_delete" formaction="{% url 'dcim:consoleport_bulk_delete' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-danger btn-sm">
+                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                </button>
+            {% endif %}
+        </div>
         {% if perms.dcim.add_consoleport %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:consoleport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleports' pk=object.pk %}" class="btn btn-sm btn-primary">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Port
                 </a>
             </div>
         {% endif %}
-        <div class="clearfix"></div>
     </div>
   </form>
   {% include 'inc/paginator.html' with paginator=consoleport_table.paginator page=consoleport_table.page %}

+ 21 - 24
netbox/templates/dcim/device/consoleserverports.html

@@ -6,37 +6,34 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end noprint">
-      {% if request.user.is_authenticated %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DeviceConsoleServerPortTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
-      {% endif %}
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="DeviceConsoleServerPortTable_config" %}
     {% render_table consoleserverport_table 'inc/table.html' %}
-    <div class="noprint">
-        {% if perms.dcim.change_consoleserverport %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-        {% endif %}
-        {% if perms.dcim.delete_consoleserverport %}
-            <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-        {% endif %}
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
+            {% if perms.dcim.change_consoleserverport %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:consoleserverport_bulk_rename' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
+                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:consoleserverport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                </button>
+                <button type="submit" name="_disconnect" formaction="{% url 'dcim:consoleserverport_bulk_disconnect' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
+                    <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+                </button>
+            {% endif %}
+            {% if perms.dcim.delete_consoleserverport %}
+                <button type="submit" formaction="{% url 'dcim:consoleserverport_bulk_delete' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-danger btn-sm">
+                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                </button>
+            {% endif %}
+        </div>
         {% if perms.dcim.add_consoleserverport %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:consoleserverport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Console Server Ports
                 </a>
             </div>
         {% endif %}
-        <div class="clearfix"></div>
     </div>
   </form>
   {% include 'inc/paginator.html' with paginator=consoleserverport_table.paginator page=consoleserverport_table.page %}

+ 18 - 21
netbox/templates/dcim/device/devicebays.html

@@ -6,34 +6,31 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end noprint">
-      {% if request.user.is_authenticated %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DeviceDeviceBayTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
-      {% endif %}
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="DeviceDeviceBayTable_config" %}
     {% render_table devicebay_table 'inc/table.html' %}
-    <div class="noprint">
-        {% if perms.dcim.change_devicebay %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-outline-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-        {% endif %}
-        {% if perms.dcim.delete_devicebay %}
-            <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-outline-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
-            </button>
-        {% endif %}
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
+            {% if perms.dcim.change_devicebay %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:devicebay_bulk_rename' %}?return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-outline-warning btn-sm">
+                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:devicebay_bulk_edit' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                </button>
+            {% endif %}
+            {% if perms.dcim.delete_devicebay %}
+                <button type="submit" formaction="{% url 'dcim:devicebay_bulk_delete' %}?return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-outline-danger btn-sm">
+                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete selected
+                </button>
+            {% endif %}
+        </div>
         {% if perms.dcim.add_devicebay %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:devicebay_add' %}?device={{ object.pk }}&return_url={{ object.get_absolute_url }}%23tab_devicebays" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Device Bays
                 </a>
             </div>
         {% endif %}
-        <div class="clearfix"></div>
     </div>
   </form>
   {% include 'inc/paginator.html' with paginator=devicebay_table.paginator page=devicebay_table.page %}

+ 21 - 24
netbox/templates/dcim/device/frontports.html

@@ -6,37 +6,34 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end noprint">
-      {% if request.user.is_authenticated %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DeviceFrontPortTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
-      {% endif %}
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="DeviceFrontPortTable_config" %}
     {% render_table frontport_table 'inc/table.html' %}
-    <div class="noprint">
-        {% if perms.dcim.change_frontport %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-        {% endif %}
-        {% if perms.dcim.delete_frontport %}
-            <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-        {% endif %}
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
+            {% if perms.dcim.change_frontport %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:frontport_bulk_rename' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
+                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:frontport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                </button>
+                <button type="submit" name="_disconnect" formaction="{% url 'dcim:frontport_bulk_disconnect' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
+                    <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+                </button>
+            {% endif %}
+            {% if perms.dcim.delete_frontport %}
+                <button type="submit" formaction="{% url 'dcim:frontport_bulk_delete' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-danger btn-sm">
+                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                </button>
+            {% endif %}
+        </div>
         {% if perms.dcim.add_frontport %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:frontport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_frontports' pk=object.pk %}" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add front ports
                 </a>
             </div>
         {% endif %}
-        <div class="clearfix"></div>
     </div>
   </form>
   {% include 'inc/paginator.html' with paginator=frontport_table.paginator page=frontport_table.page %}

+ 23 - 16
netbox/templates/dcim/device/interfaces.html

@@ -6,30 +6,37 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end col-md-4 noprint table-controls mw-33">
-        <div class="input-group input-group-sm">
-            <input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
-            <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
-                <i class="mdi mdi-table-cog"></i>
-            </button>
-            <ul class="dropdown-menu">
+    <div class="row mb-3 justify-content-between">
+        <div class="col col-md-2 float-end noprint table-controls mb-0 mw-33">
+            <div class="input-group input-group-sm">
                 {% if request.user.is_authenticated %}
                     <button
                         type="button"
-                        class="dropdown-item"
+                        class="btn btn-sm btn-outline-dark"
                         data-bs-toggle="modal"
                         data-bs-target="#DeviceInterfaceTable_config"
                         title="Configure Table">
-                        Configure Table
+                        <i class="mdi mdi-cog"></i> Configure Table
                     </button>
-                {% endif %}
-                <button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
-                <button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
-            </ul>
+                    {% endif %}
+                <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
+                    <i class="mdi mdi-eye"></i>
+                </button>
+                <ul class="dropdown-menu">
+                    <button type="button" class="dropdown-item toggle-enabled" data-state="show">Hide Enabled</button>
+                    <button type="button" class="dropdown-item toggle-disabled" data-state="show">Hide Disabled</button>
+                </ul>
+            </div>
+        </div>
+        <div class="col col-md-4 float-end noprint table-controls mw-33">
+            <div class="input-group input-group-sm">
+                <input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
+            </div>
         </div>
     </div>
     {% render_table interface_table 'inc/table.html' %}
-    <div class="noprint">
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
         {% if perms.dcim.change_interface %}
             <button type="submit" name="_rename" formaction="{% url 'dcim:interface_bulk_rename' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
                 <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
@@ -46,14 +53,14 @@
                 <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
             </button>
         {% endif %}
+        </div>
         {% if perms.dcim.add_interface %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:interface_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_interfaces' pk=object.pk %}" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Interfaces
                 </a>
             </div>
         {% endif %}
-        <div class="clearfix"></div>
     </div>
   </form>
   {% include 'inc/paginator.html' with paginator=interface_table.paginator page=interface_table.page %}

+ 18 - 20
netbox/templates/dcim/device/inventory.html

@@ -6,28 +6,26 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end noprint">
-      {% if request.user.is_authenticated %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DeviceInventoryItemTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
-      {% endif %}
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="DeviceInventoryItemTable_config" %}
     {% render_table inventoryitem_table 'inc/table.html' %}
-    <div class="noprint">
-        {% if perms.dcim.change_inventoryitem %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-        {% endif %}
-        {% if perms.dcim.delete_inventoryitem %}
-            <button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-        {% endif %}
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
+            {% if perms.dcim.change_inventoryitem %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:inventoryitem_bulk_rename' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:inventoryitem_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                </button>
+            {% endif %}
+            {% if perms.dcim.delete_inventoryitem %}
+                <button type="submit" name="_delete" formaction="{% url 'dcim:inventoryitem_bulk_delete' %}?return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-danger btn-sm">
+                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                </button>
+            {% endif %}
+        </div>
         {% if perms.dcim.add_inventoryitem %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:inventoryitem_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_inventory' pk=object.pk %}" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Inventory Item
                 </a>

+ 21 - 24
netbox/templates/dcim/device/poweroutlets.html

@@ -6,37 +6,34 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end noprint">
-      {% if request.user.is_authenticated %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DevicePowerOutletTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
-      {% endif %}
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="DevicePowerOutletTable_config" %}
     {% render_table poweroutlet_table 'inc/table.html' %}
-    <div class="noprint">
-        {% if perms.dcim.change_powerport %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-        {% endif %}
-        {% if perms.dcim.delete_poweroutlet %}
-            <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-        {% endif %}
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
+            {% if perms.dcim.change_powerport %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:poweroutlet_bulk_rename' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
+                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:poweroutlet_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                </button>
+                <button type="submit" name="_disconnect" formaction="{% url 'dcim:poweroutlet_bulk_disconnect' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
+                    <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+                </button>
+            {% endif %}
+            {% if perms.dcim.delete_poweroutlet %}
+                <button type="submit" formaction="{% url 'dcim:poweroutlet_bulk_delete' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-danger btn-sm">
+                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                </button>
+            {% endif %}
+        </div>
         {% if perms.dcim.add_poweroutlet %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:poweroutlet_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Outlets
                 </a>
             </div>
         {% endif %}
-        <div class="clearfix"></div>
     </div>
   </form>
   {% include 'inc/paginator.html' with paginator=poweroutlet_table.paginator page=poweroutlet_table.page %}

+ 22 - 25
netbox/templates/dcim/device/powerports.html

@@ -6,37 +6,34 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end noprint">
-      {% if request.user.is_authenticated %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DevicePowerPortTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
-      {% endif %}
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="DevicePowerPortTable_config" %}
     {% render_table powerport_table 'inc/table.html' %}
-    <div class="noprint">
-        {% if perms.dcim.change_powerport %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-        {% endif %}
-        {% if perms.dcim.delete_powerport %}
-            <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-        {% endif %}
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
+            {% if perms.dcim.change_powerport %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:powerport_bulk_rename' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
+                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:powerport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                </button>
+                <button type="submit" name="_disconnect" formaction="{% url 'dcim:powerport_bulk_disconnect' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
+                    <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+                </button>
+            {% endif %}
+            {% if perms.dcim.delete_powerport %}
+                <button type="submit" name="_delete" formaction="{% url 'dcim:powerport_bulk_delete' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-danger btn-sm">
+                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                </button>
+            {% endif %}
+        </div>
         {% if perms.dcim.add_powerport %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:powerport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_powerports' pk=object.pk %}" class="btn btn-sm btn-primary">
-                    <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add power port
+                    <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add Power Port
                 </a>
             </div>
         {% endif %}
-        <div class="clearfix"></div>
     </div>
   </form>
   {% include 'inc/paginator.html' with paginator=powerport_table.paginator page=powerport_table.page %}

+ 21 - 24
netbox/templates/dcim/device/rearports.html

@@ -6,37 +6,34 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end noprint">
-      {% if request.user.is_authenticated %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#DeviceRearPortTable_config" title="Configure table"><i class="mdi mdi-cog"></i> Configure</button>
-      {% endif %}
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="DeviceRearPortTable_config" %}
     {% render_table rearport_table 'inc/table.html' %}
-    <div class="noprint">
-        {% if perms.dcim.change_rearport %}
-            <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
-                <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
-            </button>
-            <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-warning btn-sm">
-                <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
-            </button>
-            <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
-                <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
-            </button>
-        {% endif %}
-        {% if perms.dcim.delete_rearport %}
-            <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm">
-                <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
-            </button>
-        {% endif %}
+    <div class="noprint bulk-buttons">
+        <div class="bulk-button-group">
+            {% if perms.dcim.change_rearport %}
+                <button type="submit" name="_rename" formaction="{% url 'dcim:rearport_bulk_rename' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-warning btn-sm">
+                    <i class="mdi mdi-pencil-outline" aria-hidden="true"></i> Rename
+                </button>
+                <button type="submit" name="_edit" formaction="{% url 'dcim:rearport_bulk_edit' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-warning btn-sm">
+                    <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit
+                </button>
+                <button type="submit" name="_disconnect" formaction="{% url 'dcim:rearport_bulk_disconnect' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-outline-danger btn-sm">
+                    <span class="mdi mdi-ethernet-cable-off" aria-hidden="true"></span> Disconnect
+                </button>
+            {% endif %}
+            {% if perms.dcim.delete_rearport %}
+                <button type="submit" formaction="{% url 'dcim:rearport_bulk_delete' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-danger btn-sm">
+                    <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete
+                </button>
+            {% endif %}
+        </div>
         {% if perms.dcim.add_rearport %}
-            <div class="float-end">
+            <div class="bulk-button-group">
                 <a href="{% url 'dcim:rearport_add' %}?device={{ object.pk }}&return_url={% url 'dcim:device_rearports' pk=object.pk %}" class="btn btn-primary btn-sm">
                     <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add rear ports
                 </a>
             </div>
         {% endif %}
-        <div class="clearfix"></div>
     </div>
   </form>
   {% include 'inc/paginator.html' with paginator=rearport_table.paginator page=rearport_table.page %}

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

@@ -23,17 +23,17 @@
     <i class="mdi mdi-file-image-outline"></i> 
     Hide Images
   </button>
-  <a {% if prev_rack %}href="{% url 'dcim:rack' pk=prev_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-sm btn-primary m-1">
+  <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 %}">
     <i class="mdi mdi-chevron-left" aria-hidden="true"></i> Previous Rack
   </a>
-  <a {% if next_rack %}href="{% url 'dcim:rack' pk=next_rack.pk %}"{% else %}disabled="disabled"{% endif %} class="btn btn-sm btn-primary m-1">
+  <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 %}">
     <i class="mdi mdi-chevron-right" aria-hidden="true"></i> Next Rack
   </a>
 {% endblock %}
 
 {% block content %}
 <div class="row">
-	<div class="col col-md-6">
+	<div class="col col-12 col-xl-6">
         <div class="card">
             <h5 class="card-header">
                 Rack
@@ -310,7 +310,7 @@
         </div>
         {% plugin_left_page object %}
 	</div>
-    <div class="col col-md-6">
+    <div class="col col-12 col-xl-6">
         <div class="row" style="margin-bottom: 20px">
             <div class="col col-md-6 col-sm-6 col-xs-12 text-center">
               <div style="margin-left: 30px">

+ 0 - 17
netbox/templates/extras/objectchange.html

@@ -15,23 +15,6 @@
   <li class="breadcrumb-item">{{ object }}</li>
 {% endblock %}
 
-{% block header %}
-    <div class="row noprint">
-        <div class="col col-sm-4 col-md-3">
-            <form action="{% url 'extras:objectchange_list' %}" method="get">
-                <div class="input-group">
-                    <input type="text" name="q" class="form-control" />
-                    <span class="input-group-btn">
-                        <button type="submit" class="btn btn-primary">
-                            <span class="mdi mdi-magnify" aria-hidden="true"></span>
-                        </button>
-                    </span>
-                </div>
-            </form>
-        </div>
-    </div>
-{% endblock %}
-
 {% block content %}
 <div class="row mb-3">
     <div class="col col-md-5">

+ 15 - 33
netbox/templates/generic/object_list.html

@@ -58,27 +58,7 @@
 {% endif %}
 
 {# Object list filter, table config #}
-<div class="row mb-3">
-  <div class="col col-md-4 offset-md-8 d-flex noprint table-controls">
-    <div class="input-group input-group-sm">
-      <input type="text" class="form-control object-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
-      {% if request.user.is_authenticated and table_config_form %}
-        <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#ObjectTable_config" title="Configure Table">
-          <i class="mdi mdi-table-eye"></i>
-        </button>
-      {% endif %}
-      {% if filter_form %}
-      <button
-        type="button"
-        class="btn btn-sm btn-outline-dark"
-        data-bs-toggle="collapse"
-        data-bs-target="#advanced-search-content">
-        Advanced Search
-      </button>
-    {% endif %}
-    </div>
-  </div>
-</div>
+{% include 'inc/table_controls.html' with table_modal="ObjectTable_config" %}
 
 {# Object table #}
 <div class="row">
@@ -91,18 +71,20 @@
           <div class="table-responsive">
             {% render_table table 'inc/table.html' %}
           </div>
-          <div class="float-start noprint bulk-buttons">
-            {% block bulk_buttons %}{% endblock %}
-            {% if bulk_edit_url and permissions.change %}
-            <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
-              <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
-            </button>
-            {% endif %}
-            {% if bulk_delete_url and permissions.delete %}
-            <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
-              <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
-            </button>
-            {% endif %}
+          <div class="noprint bulk-buttons">
+            <div class="bulk-button-group">
+              {% block bulk_buttons %}{% endblock %}
+              {% if bulk_edit_url and permissions.change %}
+                <button type="submit" name="_edit" formaction="{% url bulk_edit_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-warning btn-sm">
+                  <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit Selected
+                </button>
+              {% endif %}
+              {% if bulk_delete_url and permissions.delete %}
+                <button type="submit" name="_delete" formaction="{% url bulk_delete_url %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}" class="btn btn-danger btn-sm">
+                  <i class="mdi mdi-trash-can-outline" aria-hidden="true"></i> Delete Selected
+                </button>
+              {% endif %}
+            </div>
           </div>
         </form>
       {% else %}

+ 54 - 41
netbox/templates/home.html

@@ -2,7 +2,25 @@
 {% load get_status %}
 {% load helpers %}
 
-{% block header %}{% endblock %}
+{% block header %}
+    {{ block.super }}
+    {% if new_release %}
+        {# new_release is set only if the current user is a superuser or staff member #}
+        <div class="header-alert-container">
+          <div class="alert alert-info text-center mw-md-50" role="alert">
+            <h6 class="alert-heading">
+              <i class="mdi mdi-information-outline"></i><br/>New Release Available
+            </h6>
+            <small><a href="{{ new_release.url }}">NetBox v{{ new_release.version }}</a> is available.</small>
+            <hr class="my-2" />
+            <small class="mb-0">
+              <a href="https://netbox.readthedocs.io/en/stable/installation/upgrading/">Upgrade Instructions</a>
+            </small>
+          </div>
+        </div>
+    {% endif %}
+{% endblock %}
+
 {% block title %}Home{% endblock %}
 
 {% block content %}
@@ -35,47 +53,42 @@
   {# Changelog #}
   <div class="row my-4 flex-grow-1 changelog-container">
     <div class="col">
-      <h5 class="text-center">Changelog</h5>
-      {% if changelog and perms.extras.view_objectchange %}
-        {# TODO: Replace this with a django-tables2 Table #}
-        <table class="table align-middle table-hover">
-          <thead>
-            <tr>
-              <th scope="col">User</th>
-              <th scope="col">Action</th>
-              <th scope="col">Type</th>
-              <th scope="col">Object</th>
-              <th scope="col">Time</th>
-              <th scope="col" align="right"></th>
-            </tr>
-          </thead>
-          <tbody>
-            {% for change in changelog %}
-              <tr class="{% get_status change.get_action_display %}">
-                <th scope="row">{{ change.user|default:change.user_name }}</th>
-                <td>{{ change.get_action_display|bettertitle }}</td>
-                <td>{{ change.changed_object_type.name|bettertitle }}</td>
-                <td>
-                  {% if change.changed_object.get_absolute_url %}
-                  <a class="text-body" href="{{ change.changed_object.get_absolute_url }}">{{ change.changed_object }}</a>
-                  {% else %} {{ change.changed_object|default:change.object_repr }} {% endif %}
-                </td>
-
-                <td>{{ change.time|date:'SHORT_DATETIME_FORMAT' }}</td>
-                <td>
-                  <a role="button" class="text-body" href="{{ change.get_absolute_url }}">
-                    <i class="mdi mdi-dots-horizontal" data-bs-toggle="tooltip" data-bs-placement="left" title="View Change Details"></i>
-                  </a>
-                </td>
-              </tr>
-            {% endfor %}
-          </tbody>
-        </table>
-      {% elif perms.extras.view_objectchange %}
-        <div class="alert alert-secondary mt-4" role="alert">
-          No change history found.
+      <div class="card">
+        <h6 class="card-header text-primary text-center">Changelog</h6>
+          {% if changelog and perms.extras.view_objectchange %}
+            {# TODO: Replace this with a django-tables2 Table #}
+            <table class="table table-flush align-middle table-hover">
+              <thead>
+                <tr>
+                  <th scope="col">User</th>
+                  <th scope="col">Action</th>
+                  <th scope="col">Type</th>
+                  <th scope="col">Object</th>
+                  <th scope="col">Time</th>
+                </tr>
+              </thead>
+              <tbody>
+                {% for change in changelog %}
+                  <tr class="{% get_status change.get_action_display %}" data-href="{{ change.get_absolute_url }}">
+                    <th scope="row">{{ change.user|default:change.user_name }}</th>
+                    <td>{{ change.get_action_display|bettertitle }}</td>
+                    <td>{{ change.changed_object_type.name|bettertitle }}</td>
+                    <td>
+                      {% if change.changed_object.get_absolute_url %}
+                      <a class="text-body" href="{{ change.changed_object.get_absolute_url }}">{{ change.changed_object }}</a>
+                      {% else %} {{ change.changed_object|default:change.object_repr }} {% endif %}
+                    </td>
+                    <td>{{ change.time|date:'SHORT_DATETIME_FORMAT' }}</td>
+                  </tr>
+                {% endfor %}
+              </tbody>
+            </table>
+          {% elif perms.extras.view_objectchange %}
+            <div class="alert alert-secondary mt-4" role="alert">
+              No change history found.
+            </div>
+          {% endif %}
         </div>
-      {% endif %}
     </div>
   </div>
 {% endblock content %}

+ 37 - 0
netbox/templates/inc/table_controls.html

@@ -0,0 +1,37 @@
+<div class="row mb-3 justify-content-between">
+    <div class="col col-md-2 mb-0 d-flex noprint table-controls">
+        {% if request.user.is_authenticated %}
+            <div class="input-group input-group-sm">
+                <button
+                    type="button"
+                    data-bs-toggle="modal"
+                    title="Configure Table"
+                    data-bs-target="#{{ table_modal }}"
+                    class="btn btn-sm btn-outline-dark"
+                    >
+                    <i class="mdi mdi-cog"></i> Configure Table
+                </button>
+            </div>
+        {% endif %}
+    </div>
+    <div class="col col-md-4 d-flex noprint table-controls">
+        <div class="input-group input-group-sm">
+            <input
+                type="text"
+                class="form-control object-filter"
+                placeholder="Filter"
+                title="Filter text (regular expressions supported)"
+            />
+            {% if filter_form %}
+                <button
+                    type="button"
+                    class="btn btn-sm btn-outline-dark"
+                    data-bs-toggle="collapse"
+                    data-bs-target="#advanced-search-content"
+                >
+                    Advanced Search
+                </button>
+            {% endif %}
+        </div>
+    </div>
+</div>

+ 1 - 8
netbox/templates/virtualization/virtualmachine/interfaces.html

@@ -6,14 +6,7 @@
 {% block content %}
   <form method="post">
     {% csrf_token %}
-    <div class="float-end col-md-4 noprint table-controls mw-33">
-        <div class="input-group input-group-sm">
-            <input type="text" class="form-control interface-filter" placeholder="Filter" title="Filter text (regular expressions supported)" />
-            {% if request.user.is_authenticated %}
-                <button type="button" class="btn btn-outline-dark btn-sm" data-bs-toggle="modal" data-bs-target="#VirtualMachineVMInterfaceTable_config" title="Configure Table"><i class="mdi mdi-table-eye"></i></button>
-            {% endif %}
-        </div>
-    </div>
+    {% include 'inc/table_controls.html' with table_modal="VirtualMachineVMInterfaceTable_config" %}
     {% render_table interface_table 'inc/table.html' %}
     <div class="noprint">
         {% if perms.virtualization.change_vminterface %}

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