Procházet zdrojové kódy

bootstrap 5 class updates

checktheroads před 4 roky
rodič
revize
d1d2ad6a5c
33 změnil soubory, kde provedl 607 přidání a 526 odebrání
  1. 3 3
      netbox/ipam/tables.py
  2. 6 1
      netbox/project-static/netbox.scss
  3. 8 0
      netbox/project-static/src/clipboard.ts
  4. 5 0
      netbox/project-static/src/global.d.ts
  5. 0 1
      netbox/project-static/src/index.ts
  6. 9 1
      netbox/project-static/src/netbox.ts
  7. 62 0
      netbox/project-static/src/secrets.ts
  8. 13 7
      netbox/project-static/src/util.ts
  9. 1 1
      netbox/secrets/views.py
  10. 0 12
      netbox/templates/dcim/device/base.html
  11. 2 2
      netbox/templates/dcim/inc/cable_toggle_buttons.html
  12. 32 27
      netbox/templates/dcim/powerfeed.html
  13. 28 26
      netbox/templates/dcim/powerpanel.html
  14. 1 1
      netbox/templates/extras/object_changelog.html
  15. 3 11
      netbox/templates/generic/object_edit.html
  16. 2 2
      netbox/templates/ipam/routetarget.html
  17. 36 30
      netbox/templates/ipam/vlan.html
  18. 44 26
      netbox/templates/ipam/vlangroup_edit.html
  19. 33 16
      netbox/templates/ipam/vlangroup_vlans.html
  20. 2 2
      netbox/templates/ipam/vrf.html
  21. 3 2
      netbox/templates/profile_button.html
  22. 33 44
      netbox/templates/secrets/secret.html
  23. 76 87
      netbox/templates/secrets/secret_edit.html
  24. 20 22
      netbox/templates/users/api_tokens.html
  25. 12 25
      netbox/templates/users/base.html
  26. 9 17
      netbox/templates/users/change_password.html
  27. 23 5
      netbox/templates/users/profile.html
  28. 6 6
      netbox/templates/users/userkey.html
  29. 18 23
      netbox/templates/users/userkey_edit.html
  30. 3 3
      netbox/templates/utilities/render_errors.html
  31. 75 73
      netbox/templates/virtualization/cluster.html
  32. 11 18
      netbox/templates/virtualization/cluster_add_devices.html
  33. 28 32
      netbox/templates/virtualization/vminterface_edit.html

+ 3 - 3
netbox/ipam/tables.py

@@ -33,7 +33,7 @@ IPADDRESS_LINK = """
 {% if record.pk %}
     <a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
 {% elif perms.ipam.add_ipaddress %}
-    <a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
+    <a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-sm btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
 {% else %}
     {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available
 {% endif %}
@@ -65,7 +65,7 @@ VLAN_LINK = """
 {% if record.pk %}
     <a href="{{ record.get_absolute_url }}">{{ record.vid }}</a>
 {% elif perms.ipam.add_vlan %}
-    <a href="{% url 'ipam:vlan_add' %}?vid={{ record.vid }}&group={{ vlan_group.pk }}{% if vlan_group.site %}&site={{ vlan_group.site.pk }}{% endif %}" class="btn btn-xs btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available</a>
+    <a href="{% url 'ipam:vlan_add' %}?vid={{ record.vid }}&group={{ vlan_group.pk }}{% if vlan_group.site %}&site={{ vlan_group.site.pk }}{% endif %}" class="btn btn-sm btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available</a>
 {% else %}
     {{ record.available }} VLAN{{ record.available|pluralize }} available
 {% endif %}
@@ -90,7 +90,7 @@ VLAN_ROLE_LINK = """
 VLANGROUP_ADD_VLAN = """
 {% with next_vid=record.get_next_available_vid %}
     {% if next_vid and perms.ipam.add_vlan %}
-        <a href="{% url 'ipam:vlan_add' %}?group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-xs btn-success">
+        <a href="{% url 'ipam:vlan_add' %}?group={{ record.pk }}&vid={{ next_vid }}" title="Add VLAN" class="btn btn-sm btn-success">
             <i class="mdi mdi-plus-thick" aria-hidden="true"></i>
         </a>
     {% endif %}

+ 6 - 1
netbox/project-static/netbox.scss

@@ -245,7 +245,8 @@ span.color-label {
 }
 
 textarea#id_local_context_data,
-textarea.markdown {
+textarea.markdown,
+textarea#id_public_key {
   font-family: $font-family-monospace;
 }
 
@@ -355,3 +356,7 @@ span.bi-plus:before {
 table tbody tr.success {
   background-color: rgba($success, 0.15);
 }
+table td,
+table th {
+  font-size: $font-size-sm;
+}

+ 8 - 0
netbox/project-static/src/clipboard.ts

@@ -0,0 +1,8 @@
+import Clipboard from 'clipboard';
+import { getElements } from './util';
+
+export function initClipboard() {
+  for (const element of getElements('a.copy-token', 'button.copy-secret')) {
+    new Clipboard(element);
+  }
+}

+ 5 - 0
netbox/project-static/src/global.d.ts

@@ -26,6 +26,11 @@ type APIObjectBase = {
   [k: string]: JSONAble;
 };
 
+type APIKeyPair = {
+  public_key: string;
+  private_key: string;
+};
+
 type APIReference = {
   id: number;
   name: string;

+ 0 - 1
netbox/project-static/src/index.ts

@@ -1,5 +1,4 @@
 import 'babel-polyfill';
 import '@popperjs/core';
 import 'bootstrap';
-import 'clipboard';
 import './netbox';

+ 9 - 1
netbox/project-static/src/netbox.ts

@@ -1,11 +1,13 @@
-import { Tooltip } from 'bootstrap';
+import { Modal, Tooltip } from 'bootstrap';
 import Masonry from 'masonry-layout';
 import { initApiSelect, initStaticSelect, initColorSelect } from './select';
 import { initDateSelector } from './dateSelector';
 import { initMessageToasts } from './toast';
 import { initSpeedSelector, initForms } from './forms';
 import { initRackElevation } from './buttons';
+import { initClipboard } from './clipboard';
 import { initSearchBar } from './search';
+// import { initGenerateKeyPair } from './secrets';
 import { getElements } from './util';
 
 const INITIALIZERS = [
@@ -18,6 +20,8 @@ const INITIALIZERS = [
   initSpeedSelector,
   initColorSelect,
   initRackElevation,
+  initClipboard,
+  // initGenerateKeyPair,
 ] as (() => void)[];
 
 /**
@@ -30,6 +34,10 @@ function initBootstrap(): void {
     for (const tooltip of getElements('[data-bs-toggle="tooltip"]')) {
       new Tooltip(tooltip, { container: 'body', boundary: 'window' });
     }
+    for (const modal of getElements('[data-bs-toggle="modal"]')) {
+      // for (const modal of getElements('div.modal')) {
+      new Modal(modal);
+    }
     initMessageToasts();
     initForms();
   }

+ 62 - 0
netbox/project-static/src/secrets.ts

@@ -0,0 +1,62 @@
+import { apiGetBase, getElements, isApiError } from './util';
+/**
+ * 
+ * $('#generate_keypair').click(function() {
+        $('#new_keypair_modal').modal('show');
+        $.ajax({
+            url: netbox_api_path + 'secrets/generate-rsa-key-pair/',
+            type: 'GET',
+            dataType: 'json',
+            success: function (response, status) {
+                var public_key = response.public_key;
+                var private_key = response.private_key;
+                $('#new_pubkey').val(public_key);
+                $('#new_privkey').val(private_key);
+            },
+            error: function (xhr, ajaxOptions, thrownError) {
+                alert("There was an error generating a new key pair.");
+            }
+        });
+    });
+ */
+export function initGenerateKeyPair() {
+  const element = document.getElementById('new_keypair_modal') as HTMLDivElement;
+  const accept = document.getElementById('use_new_pubkey') as HTMLButtonElement;
+  const publicElem = element.querySelector<HTMLTextAreaElement>('textarea#new_pubkey');
+  const privateElem = element.querySelector<HTMLTextAreaElement>('textarea#new_privkey');
+
+  function handleOpen() {
+    for (const elem of [publicElem, privateElem]) {
+      if (elem !== null) {
+        elem.setAttribute('readonly', '');
+      }
+    }
+
+    apiGetBase<APIKeyPair>('/api/secrets/generate-rsa-key-pair').then(data => {
+      if (!isApiError(data)) {
+        const { private_key: priv, public_key: pub } = data;
+        if (publicElem !== null && privateElem !== null) {
+          publicElem.value = pub;
+          privateElem.value = priv;
+        }
+      }
+    });
+  }
+  function handleAccept() {
+    const publicKeyField = document.getElementById('id_public_key') as HTMLTextAreaElement;
+    if (publicElem !== null) {
+      publicKeyField.value = publicElem.value;
+      publicKeyField.innerText = publicElem.value;
+    }
+  }
+  element.addEventListener('shown.bs.modal', handleOpen);
+  accept.addEventListener('click', handleAccept);
+}
+
+export function initLockUnlock() {
+  for (const element of getElements<HTMLButtonElement>('button.unlock-secret')) {
+    function handleClick() {
+      const { secretId } = element.dataset;
+    }
+  }
+}

+ 13 - 7
netbox/project-static/src/util.ts

@@ -32,22 +32,28 @@ export function getCsrfToken(): string {
   return csrfToken;
 }
 
-/**
- * Fetch data from the NetBox API (authenticated).
- * @param url API endpoint
- */
-export async function getApiData<T extends APIObjectBase>(
+export async function apiGetBase<T extends Record<string, unknown>>(
   url: string,
-): Promise<APIAnswer<T> | APIError> {
+): Promise<T | APIError> {
   const token = getCsrfToken();
   const res = await fetch(url, {
     method: 'GET',
     headers: { 'X-CSRFToken': token },
   });
-  const json = (await res.json()) as APIAnswer<T> | APIError;
+  const json = (await res.json()) as T | APIError;
   return json;
 }
 
+/**
+ * Fetch data from the NetBox API (authenticated).
+ * @param url API endpoint
+ */
+export async function getApiData<T extends APIObjectBase>(
+  url: string,
+): Promise<APIAnswer<T> | APIError> {
+  return await apiGetBase<APIAnswer<T>>(url);
+}
+
 export function getElements<K extends keyof SVGElementTagNameMap>(
   ...key: K[]
 ): Generator<SVGElementTagNameMap[K]>;

+ 1 - 1
netbox/secrets/views.py

@@ -73,7 +73,7 @@ class SecretListView(generic.ObjectListView):
     filterset = filters.SecretFilterSet
     filterset_form = forms.SecretFilterForm
     table = tables.SecretTable
-    action_buttons = ('import', 'export')
+    action_buttons = ('add', 'import', 'export')
 
 
 class SecretView(generic.ObjectView):

+ 0 - 12
netbox/templates/dcim/device/base.html

@@ -215,16 +215,4 @@
         </a>
     </li>
 {% endif %}
-{% if perms.extras.view_objectchange %}
-    <li 
-        role="presentation"
-        class="nav-item">
-        <a
-            href="{% url 'dcim:device_changelog' pk=object.pk %}"
-            class="nav-link{% if active_tab == 'changelog' %} active{% endif %}"
-        >
-            Change Log
-        </a>
-    </li>
-{% endif %}
 {% endblock %}

+ 2 - 2
netbox/templates/dcim/inc/cable_toggle_buttons.html

@@ -1,10 +1,10 @@
 {% if perms.dcim.change_cable %}
     {% if cable.status == 'connected' %}
-        <a href="#" class="btn btn-warning btn-xs cable-toggle connected" title="Mark planned" data="{{ cable.pk }}">
+        <a href="#" class="btn btn-warning btn-sm cable-toggle connected" title="Mark Planned" data="{{ cable.pk }}">
             <i class="mdi mdi-lan-disconnect" aria-hidden="true"></i>
         </a>
     {% else %}
-        <a href="#" class="btn btn-success btn-xs cable-toggle" title="Mark installed" data="{{ cable.pk }}">
+        <a href="#" class="btn btn-success btn-sm cable-toggle" title="Mark Installed" data="{{ cable.pk }}">
             <i class="mdi mdi-lan-connect" aria-hidden="true"></i>
         </a>
     {% endif %}

+ 32 - 27
netbox/templates/dcim/powerfeed.html

@@ -6,13 +6,13 @@
 {% load plugins %}
 
 {% block breadcrumbs %}
-  <li><a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a></li>
-  <li><a href="{{ object.power_panel.site.get_absolute_url }}">{{ object.power_panel.site }}</a></li>
-  <li><a href="{{ object.power_panel.get_absolute_url }}">{{ object.power_panel }}</a></li>
+  <li class="breadcrumb-item"><a href="{% url 'dcim:powerfeed_list' %}">Power Feeds</a></li>
+  <li class="breadcrumb-item"><a href="{{ object.power_panel.site.get_absolute_url }}">{{ object.power_panel.site }}</a></li>
+  <li class="breadcrumb-item"><a href="{{ object.power_panel.get_absolute_url }}">{{ object.power_panel }}</a></li>
   {% if object.rack %}
-    <li><a href="{{ object.rack.get_absolute_url }}">{{ object.rack }}</a></li>
+    <li class="breadcrumb-item"><a href="{{ object.rack.get_absolute_url }}">{{ object.rack }}</a></li>
   {% endif %}
-  <li>{{ object }}</li>
+  <li class="breadcrumb-item">{{ object }}</li>
 {% endblock %}
 
 {% block content %}
@@ -25,13 +25,13 @@
             <div class="card-body">
                 <table class="table table-hover attr-table">
                     <tr>
-                        <td>Power Panel</td>
+                        <th scope="row">Power Panel</th>
                         <td>
                             <a href="{{ object.power_panel.get_absolute_url }}">{{ object.power_panel }}</a>
                         </td>
                     </tr>
                     <tr>
-                        <td>Rack</td>
+                        <th scope="row">Rack</th>
                         <td>
                             {% if object.rack %}
                                 <a href="{{ object.rack.get_absolute_url }}">{{ object.rack }}</a>
@@ -41,19 +41,19 @@
                         </td>
                     </tr>
                     <tr>
-                        <td>Type</td>
+                        <th scope="row">Type</th>
                         <td>
                             <span class="badge bg-{{ object.get_type_class }}">{{ object.get_type_display }}</span>
                         </td>
                     </tr>
                     <tr>
-                        <td>Status</td>
+                        <th scope="row">Status</th>
                         <td>
                             <span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
                         </td>
                     </tr>
                     <tr>
-                        <td>Connected Device</td>
+                        <th scope="row">Connected Device</th>
                         <td>
                             {% if object.connected_endpoint %}
                                 <a href="{{ object.connected_endpoint.device.get_absolute_url }}">{{ object.connected_endpoint.device }}</a> ({{ object.connected_endpoint }})
@@ -63,7 +63,7 @@
                         </td>
                     </tr>
                     <tr>
-                        <td>Utilization (Allocated)</td>
+                        <th scope="row">Utilization (Allocated)</th>
                         {% with utilization=object.connected_endpoint.get_power_draw %}
                             {% if utilization %}
                                 <td>
@@ -87,23 +87,23 @@
             <div class="card-body">
                 <table class="table table-hover attr-table">
                     <tr>
-                        <td>Supply</td>
+                        <th scope="row">Supply</th>
                         <td>{{ object.get_supply_display }}</td>
                     </tr>
                     <tr>
-                        <td>Voltage</td>
+                        <th scope="row">Voltage</th>
                         <td>{{ object.voltage }}V</td>
                     </tr>
                     <tr>
-                        <td>Amperage</td>
+                        <th scope="row">Amperage</th>
                         <td>{{ object.amperage }}A</td>
                     </tr>
                     <tr>
-                        <td>Phase</td>
+                        <th scope="row">Phase</th>
                         <td>{{ object.get_phase_display }}</td>
                     </tr>
                     <tr>
-                        <td>Max Utilization</td>
+                        <th scope="row">Max Utilization</th>
                         <td>{{ object.max_utilization }}%</td>
                     </tr>
                 </table>
@@ -126,7 +126,7 @@
             {% elif object.cable %}
                 <table class="table table-hover attr-table">
                     <tr>
-                        <td>Cable</td>
+                        <th scope="row">Cable</th>
                         <td>
                             <a href="{{ object.cable.get_absolute_url }}">{{ object.cable }}</a>
                             <a href="{% url 'dcim:powerfeed_trace' pk=object.pk %}" class="btn btn-primary btn-xs" title="Trace">
@@ -136,27 +136,27 @@
                     </tr>
                     {% if object.connected_endpoint %}
                         <tr>
-                            <td>Device</td>
+                            <th scope="row">Device</th>
                             <td>
                                 <a href="{{ object.connected_endpoint.device.get_absolute_url }}">{{ object.connected_endpoint.device }}</a>
                             </td>
                         </tr>
                         <tr>
-                            <td>Name</td>
+                            <th scope="row">Name</th>
                             <td>
                                 <a href="{{ object.connected_endpoint.get_absolute_url }}">{{ object.connected_endpoint.name }}</a>
                             </td>
                         </tr>
                         <tr>
-                            <td>Type</td>
+                            <th scope="row">Type</th>
                             <td>{{ object.connected_endpoint.get_type_display|placeholder }}</td>
                         </tr>
                         <tr>
-                            <td>Description</td>
+                            <th scope="row">Description</th>
                             <td>{{ object.connected_endpoint.description|placeholder }}</td>
                         </tr>
                         <tr>
-                            <td>Path Status</td>
+                            <th scope="row">Path Status</th>
                             <td>
                                 {% if object.path.is_active %}
                                     <span class="badge bg-success">Reachable</span>
@@ -169,15 +169,20 @@
                 </table>
             {% else %}
                 <div class="text-muted">
-                    {% if perms.dcim.add_cable %}
-                        <a href="{% url 'dcim:powerfeed_connect' termination_a_id=object.pk termination_b_type='power-port' %}?return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm pull-right">
-                            <span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
-                        </a>
-                    {% endif %}
                     Not connected
                 </div>
             {% endif %}
             </div>
+            {% if not object.mark_connected and not object.cable %}
+            <div class="card-footer">
+            {% if perms.dcim.add_cable %}
+                <a href="{% url 'dcim:powerfeed_connect' termination_a_id=object.pk termination_b_type='power-port' %}?return_url={{ object.get_absolute_url }}"
+                class="btn btn-primary btn-sm float-end">
+                    <i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
+                </a>
+                    {% endif %}
+            </div>
+            {% endif %}
         </div>
         <div class="card">
             <h5 class="card-header">

+ 28 - 26
netbox/templates/dcim/powerpanel.html

@@ -3,39 +3,41 @@
 {% load plugins %}
 
 {% block breadcrumbs %}
-  <li><a href="{% url 'dcim:powerpanel_list' %}">Power Panels</a></li>
-  <li><a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a></li>
+  <li class="breadcrumb-item"><a href="{% url 'dcim:powerpanel_list' %}">Power Panels</a></li>
+  <li class="breadcrumb-item"><a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a></li>
   {% if object.location %}
-    <li><a href="{{ object.location.get_absolute_url }}">{{ object.location }}</a></li>
+    <li class="breadcrumb-item"><a href="{{ object.location.get_absolute_url }}">{{ object.location }}</a></li>
   {% endif %}
-  <li>{{ object }}</li>
+  <li class="breadcrumb-item">{{ object }}</li>
 {% endblock %}
 
 {% block content %}
 <div class="row">
 	<div class="col-md-6">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Power Panel</strong>
+        <div class="card">
+            <h5 class="card-header">
+                Power Panel
+            </h5>
+            <div class="card-body">
+                <table class="table table-hover attr-table">
+                    <tr>
+                        <th scope="row">Site</th>
+                        <td>
+                            <a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Location</th>
+                        <td>
+                            {% if object.location %}
+                                <a href="{{ object.location.get_absolute_url }}">{{ object.location }}</a>
+                            {% else %}
+                                <span class="text-muted">None</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                </table>
             </div>
-            <table class="table table-hover panel-body attr-table">
-                <tr>
-                    <td>Site</td>
-                    <td>
-                        <a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
-                    </td>
-                </tr>
-                <tr>
-                    <td>Location</td>
-                    <td>
-                        {% if object.location %}
-                            <a href="{{ object.location.get_absolute_url }}">{{ object.location }}</a>
-                        {% else %}
-                            <span class="text-muted">None</span>
-                        {% endif %}
-                    </td>
-                </tr>
-            </table>
         </div>
         {% plugin_left_page object %}
     </div>
@@ -45,7 +47,7 @@
         {% plugin_right_page object %}
     </div>
 </div>
-<div class="row">
+<div class="row my-3">
     <div class="col-md-12">
         {% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %}
         {% plugin_full_width_page object %}

+ 1 - 1
netbox/templates/extras/object_changelog.html

@@ -1,6 +1,6 @@
 {% extends base_template %}
 
-{% block title %}{{ block.super }} - Change Log{% endblock %}
+{% block title %}{{ object }} - Change Log{% endblock %}
 
 {% block content %}
     {% include 'panel_table.html' %}

+ 3 - 11
netbox/templates/generic/object_edit.html

@@ -7,8 +7,8 @@
 <button
   type="button"
   class="btn btn-sm btn-outline-secondary"
-  data-toggle="modal"
-  data-target="#docs_modal"
+  data-bs-toggle="modal"
+  data-bs-target="#docs_modal"
   title="Help"
 >
   <i class="bi bi-question"></i>
@@ -22,15 +22,7 @@
   {% for field in form.hidden_fields %}{{ field }}{% endfor %}
   <div class="row">
     <div class="col-md-8 col-md-offset-3">
-      {% block tabs %}{% endblock %} {% if form.non_field_errors %}
-      <div class="card bg-danger">
-        <h5 class="card-header">Errors</h5>
-        <div class="card-body">
-          {{ form.non_field_errors }}
-        </div>
-      </div>
-      {% endif %} 
-      
+      {% block tabs %}{% endblock %}
       {% block form %}
       {% if form.Meta.fieldsets %}
 

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

@@ -3,8 +3,8 @@
 {% load plugins %}
 
 {% block breadcrumbs %}
-  <li><a href="{% url 'ipam:routetarget_list' %}">Route Targets</a></li>
-  <li>{{ object }}</li>
+  <li class="breadcrumb-item"><a href="{% url 'ipam:routetarget_list' %}">Route Targets</a></li>
+  <li class="breadcrumb-item">{{ object }}</li>
 {% endblock %}
 
 {% block content %}

+ 36 - 30
netbox/templates/ipam/vlan.html

@@ -15,23 +15,30 @@
   <li class="breadcrumb-item">{{ object }}</li>
 {% endblock %}
 
-{% block tabs %}
-  <ul class="nav nav-tabs" style="margin-bottom: 20px">
-    <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
-      <a href="{% url 'ipam:vlan' pk=object.pk %}">VLAN</a>
-    </li>
-    <li role="presentation"{% if active_tab == 'interfaces' %} class="active"{% endif %}>
-      <a href="{% url 'ipam:vlan_interfaces' pk=object.pk %}">Device Interfaces <span class="badge">{{ object.get_interfaces.count }}</span></a>
-    </li>
-    <li role="presentation"{% if active_tab == 'vminterfaces' %} class="active"{% endif %}>
-      <a href="{% url 'ipam:vlan_vminterfaces' pk=object.pk %}">VM Interfaces <span class="badge">{{ object.get_vminterfaces.count }}</span></a>
-    </li>
-    {% if perms.extras.view_objectchange %}
-      <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
-        <a href="{% url 'ipam:vlan_changelog' pk=object.pk %}">Change Log</a>
-      </li>
-    {% endif %}
-  </ul>
+{% block tab_items %}
+<li class="nav-item" role="presentation">
+    <a class="nav-link{% if not active_tab %} active{% endif %}" href="{% url 'ipam:vlan' pk=object.pk %}">VLAN</a>
+</li>
+<li class="nav-item" role="presentation">
+    <a class="nav-link{% if active_tab == 'interfaces' %} active{% endif %}" href="{% url 'ipam:vlan_interfaces' pk=object.pk %}">
+        Device Interfaces
+        {% with count=object.get_interfaces.count %}
+        {% if count > 0 %}
+            <span class="badge bg-primary">{{ count }}</span>
+        {% endif %}
+        {% endwith %}
+    </a>
+</li>
+<li class="nav-item" role="presentation">
+    <a class="nav-link{% if active_tab == 'vminterfaces' %} active{% endif %}" href="{% url 'ipam:vlan_vminterfaces' pk=object.pk %}">
+        VM Interfaces
+        {% comment %} {% with count=object.get_vminterfaces.count %}
+        {% if count > 0 %}
+            <span class="badge bg-primary">{{ count }}</span>
+        {% endif %}
+        {% endwith %} {% endcomment %}
+    </a>
+</li>
 {% endblock %}
 
 {% block content %}
@@ -44,7 +51,12 @@
                 <div class="card-body">
                     <table class="table table-hover attr-table">
                         <tr>
-                            <td>Site</td>
+                            <td colspan="2">
+                                <span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
+                            </td>
+                        </tr>
+                        <tr>
+                            <th scope="row">Site</th>
                             <td>
                                 {% if object.site %}
                                     {% if object.site.region %}
@@ -57,7 +69,7 @@
                             </td>
                         </tr>
                         <tr>
-                            <td>Group</td>
+                            <th scope="row">Group</th>
                             <td>
                                 {% if object.group %}
                                     <a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
@@ -67,15 +79,15 @@
                             </td>
                         </tr>
                         <tr>
-                            <td>VLAN ID</td>
+                            <th scope="row">VLAN ID</th>
                             <td>{{ object.vid }}</td>
                         </tr>
                         <tr>
-                            <td>Name</td>
+                            <th scope="row">Name</th>
                             <td>{{ object.name }}</td>
                         </tr>
                         <tr>
-                            <td>Tenant</td>
+                            <th scope="row">Tenant</th>
                             <td>
                                 {% if object.tenant %}
                                     {% if object.tenant.group %}
@@ -88,13 +100,7 @@
                             </td>
                         </tr>
                         <tr>
-                            <td>Status</td>
-                            <td>
-                                <span class="badge bg-{{ object.get_status_class }}">{{ object.get_status_display }}</span>
-                            </td>
-                        </tr>
-                        <tr>
-                            <td>Role</td>
+                            <th scope="row">Role</th>
                             <td>
                                 {% if object.role %}
                                     <a href="{% url 'ipam:vlan_list' %}?role={{ object.role.slug }}">{{ object.role }}</a>
@@ -104,7 +110,7 @@
                             </td>
                         </tr>
                         <tr>
-                            <td>Description</td>
+                            <th scope="row">Description</th>
                             <td>{{ object.description|placeholder }}</td>
                         </tr>
                     </table>

+ 44 - 26
netbox/templates/ipam/vlangroup_edit.html

@@ -3,25 +3,45 @@
 {% load helpers %}
 
 {% block form %}
-    <div class="panel panel-default">
-        <div class="panel-heading"><strong>VLAN Group</strong></div>
-        <div class="panel-body">
-            {% render_field form.name %}
-            {% render_field form.slug %}
-            {% render_field form.description %}
-        </div>
+    <div class="field-group">
+        <h4>VLAN Group</h4>
+        {% render_field form.name %}
+        {% render_field form.slug %}
+        {% render_field form.description %}
     </div>
-    <div class="panel panel-default">
-        <div class="panel-heading">
-            <strong>Scope</strong>
-        </div>
-        <div class="panel-body">
-            {% with virtual_tab_active=form.initial.cluster %}
-                <ul class="nav nav-tabs" role="tablist">
-                    <li role="presentation"{% if not virtual_tab_active %} class="active"{% endif %}><a href="#physical" role="tab" data-toggle="tab">Physical</a></li>
-                    <li role="presentation"{% if virtual_tab_active %} class="active"{% endif %}><a href="#virtual" role="tab" data-toggle="tab">Virtual</a></li>
-                </ul>
-                <div class="tab-content">
+    <div class="field-group">
+        <h4>Scope</h4>
+        {% with virtual_tab_active=form.initial.cluster %}
+            <ul class="nav nav-tabs" role="tablist">
+                <li class="nav-item" role="presentation">
+                    <button
+                        role="tab"
+                        type="button"
+                        id="physical_tab"
+                        data-bs-toggle="tab"
+                        aria-controls="physical"
+                        data-bs-target="#physical"
+                        class="nav-link {% if not virtual_tab_active %}active{% endif %}"
+                    >
+                        Physical
+                    </button>
+                </li>
+                <li class="nav-item" role="presentation">
+                    <button
+                        role="tab"
+                        type="button"
+                        id="virtual_tab"
+                        data-bs-toggle="tab"
+                        aria-controls="virtual"
+                        data-bs-target="#virtual"
+                        class="nav-link {% if virtual_tab_active %}active{% endif %}"
+                    >
+                        Virtual
+                    </button>
+                </li>
+            </ul>
+            <div class="card my-3">
+                <div class="card-body tab-content">
                     <div class="tab-pane{% if not virtual_tab_active %} active{% endif %}" id="physical">
                         {% render_field form.region %}
                         {% render_field form.site_group %}
@@ -33,17 +53,15 @@
                         {% render_field form.cluster_group %}
                         {% render_field form.cluster %}
                     </div>
+                    <span class="form-text">The VLAN group will be limited in scope to the most-specific object selected above.</span>
                 </div>
-                <span class="help-block">The VLAN group will be limited in scope to the most-specific object selected above.</span>
-            {% endwith %}
-        </div>
+            </div>
+        {% endwith %}
     </div>
     {% if form.custom_fields %}
-        <div class="panel panel-default">
-            <div class="panel-heading"><strong>Custom Fields</strong></div>
-            <div class="panel-body">
-                {% render_custom_fields form %}
-            </div>
+        <div class="field-group">
+            <h4>Custom Fields</h4>
+            {% render_custom_fields form %}
         </div>
     {% endif %}
 {% endblock %}

+ 33 - 16
netbox/templates/ipam/vlangroup_vlans.html

@@ -1,24 +1,41 @@
-{% extends 'base.html' %}
+{% extends 'generic/object.html' %}
 
 {% block title %}{{ object }} - VLANs{% endblock %}
 
+{% block controls %}
+{% if perms.ipam.add_vlan and first_available_vlan %}
+<a
+    href="{% url 'ipam:vlan_add' %}?vid={{ first_available_vlan }}&group={{ object.pk }}{% if object.site %}&site={{ object.site.pk }}{% endif %}"
+    class="btn btn-sm btn-success m-1">
+        <i class="bi bi-plus" aria-hidden="true"></i> Add a VLAN
+</a>
+{% endif %}
+{% if perms.ipam.change_vlangroup %}
+<a
+    href="{% url 'ipam:vlangroup_edit' pk=object.pk %}"
+    class="btn btn-sm btn-warning m-1">
+        <i class="mdi mdi-pencil" aria-hidden="true"></i> Edit this VLAN Group
+</a>
+{% endif %}
+{% endblock %}
+
+{% block breadcrumbs %}
+<li class="breadcrumb-item">
+    <a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a>
+</li>
+{% if object.site %}
+<li class="breadcrumb-item">
+    <a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
+</li>
+{% endif %}
+<li class="breadcrumb-item">{{ object }}</li>
+{% endblock %}
+
 {% block content %}
-<div class="row noprint">
-    <div class="col-sm-12 col-md-12">
-        <ol class="breadcrumb">
-            <li><a href="{% url 'ipam:vlangroup_list' %}">VLAN Groups</a></li>
-            {% if object.site %}
-                <li><a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a></li>
-            {% endif %}
-            <li>{{ object }}</li>
-        </ol>
+<div class="row">
+    <div class="col-md-12">
+        {% include 'utilities/obj_table.html' with table=vlan_table table_template='panel_table.html' heading='VLANs' bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
     </div>
 </div>
-    {% include 'ipam/inc/vlangroup_header.html' %}
-    <div class="row">
-        <div class="col-md-12">
-            {% include 'utilities/obj_table.html' with table=vlan_table table_template='panel_table.html' heading='VLANs' bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %}
-        </div>
-    </div>
 {% endblock %}
 

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

@@ -7,8 +7,8 @@
 {% block title %}VRF {{ object }}{% endblock %}
 
 {% block breadcrumbs %}
-  <li><a href="{% url 'ipam:vrf_list' %}">VRFs</a></li>
-  <li>{{ object }}</li>
+  <li class="breadcrumb-item"><a href="{% url 'ipam:vrf_list' %}">VRFs</a></li>
+  <li class="breadcrumb-item">{{ object }}</li>
 {% endblock %}
 
 {% block content %}

+ 3 - 2
netbox/templates/profile_button.html

@@ -15,11 +15,12 @@
       <a class="text-decoration-none" href="{% url 'admin:index' %}">
         <i class="bi bi-gear-wide-connected"></i> Admin
       </a>
-      {% else %}
+      {% endif %}
+    </li>
+    <li class="dropdown-item">
       <a class="text-decoration-none" href="{% url 'user:profile' %}">
         <i class="bi bi-person-fill"></i> Profile
       </a>
-      {% endif %}
     </li>
     <li><hr class="dropdown-divider" /></li>
     <li class="dropdown-item">

+ 33 - 44
netbox/templates/secrets/secret.html

@@ -5,54 +5,47 @@
 {% load plugins %}
 
 {% block breadcrumbs %}
-  <li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
-  <li><a href="{% url 'secrets:secret_list' %}?role={{ object.role.slug }}">{{ object.role }}</a></li>
-  <li><a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}</a></li>
-  <li>{{ object }}</li>
-{% endblock %}
-
-{% block buttons %}
-  {% if perms.secrets.change_secret %}
-    {% edit_button object %}
-  {% endif %}
-  {% if perms.secrets.delete_secret %}
-    {% delete_button object %}
-  {% endif %}
+  <li class="breadcrumb-item"><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
+  <li class="breadcrumb-item"><a href="{% url 'secrets:secret_list' %}?role={{ object.role.slug }}">{{ object.role }}</a></li>
+  <li class="breadcrumb-item"><a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}</a></li>
+  <li class="breadcrumb-item">{{ object }}</li>
 {% endblock %}
 
 {% block content %}
 <div class="row">
 	<div class="col-md-6">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Secret Attributes</strong>
+        <div class="card">
+            <h5 class="card-header">
+                Secret Attributes
+            </h5>
+            <div class="card-body">
+                <table class="table table-hover">
+                    <tr>
+                        <th scope="row">Assigned object</th>
+                        <td>
+                            <a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}</a>
+                        </td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Role</th>
+                        <td>{{ object.role }}</td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Name</th>
+                        <td>{{ object.name|placeholder }}</td>
+                    </tr>
+                </table>
             </div>
-            <table class="table table-hover panel-body">
-                <tr>
-                    <td>Assigned object</td>
-                    <td>
-                        <a href="{{ object.assigned_object.get_absolute_url }}">{{ object.assigned_object }}</a>
-                    </td>
-                </tr>
-                <tr>
-                    <td>Role</td>
-                    <td>{{ object.role }}</td>
-                </tr>
-                <tr>
-                    <td>Name</td>
-                    <td>{{ object.name|placeholder }}</td>
-                </tr>
-            </table>
         </div>
         {% include 'inc/custom_fields_panel.html' %}
         {% plugin_left_page object %}
 	</div>
 	<div class="col-md-6">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Secret Data</strong>
-            </div>
-            <div class="panel-body">
+        <div class="card">
+            <h5 class="card-header">
+                Secret Data
+            </h5>
+            <div class="card-body">
                 <form id="secret_form">
                     {% csrf_token %}
                 </form>
@@ -60,13 +53,13 @@
                     <div class="col-md-2">Secret</div>
                     <div class="col-md-6" id="secret_{{ object.pk }}">********</div>
                     <div class="col-md-4 text-right noprint">
-                        <button class="btn btn-xs btn-success unlock-secret" secret-id="{{ object.pk }}">
+                        <button class="btn btn-sm btn-success unlock-secret" secret-id="{{ object.pk }}">
                             <i class="mdi mdi-lock"></i> Unlock
                         </button>
-                        <button class="btn btn-xs btn-default copy-secret collapse" secret-id="{{ object.pk }}" data-clipboard-target="#secret_{{ object.pk }}">
+                        <button class="btn btn-sm btn-default copy-secret collapse" secret-id="{{ object.pk }}" data-clipboard-target="#secret_{{ object.pk }}">
                             <i class="mdi mdi-content-copy"></i> Copy
                         </button>
-                        <button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ object.pk }}">
+                        <button class="btn btn-sm btn-danger lock-secret collapse" secret-id="{{ object.pk }}">
                             <i class="mdi mdi-lock-open"></i> Lock
                         </button>
                     </div>
@@ -85,7 +78,3 @@
 
 {% include 'secrets/inc/private_key_modal.html' %}
 {% endblock %}
-
-{% block javascript %}
-<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
-{% endblock %}

+ 76 - 87
netbox/templates/secrets/secret_edit.html

@@ -1,99 +1,88 @@
-{% extends 'base.html' %}
+{% extends 'generic/object_edit.html' %}
 {% load static %}
 {% load form_helpers %}
 
-{% block content %}
-<form action="." method="post" class="form form-horizontal">
-    {% csrf_token %}
-    {{ form.private_key }}
-    <div class="row">
-        <div class="col-md-6 col-md-offset-3">
-            <h3>{% block title %}{% if obj.pk %}Editing {{ obj }}{% else %}Add a Secret{% endif %}{% endblock %}</h3>
-            {% if form.non_field_errors %}
-                <div class="panel panel-danger">
-                    <div class="panel-heading"><strong>Errors</strong></div>
-                    <div class="panel-body">
-                        {{ form.non_field_errors }}
-                    </div>
-                </div>
-            {% endif %}
-            <div class="panel panel-default">
-                <div class="panel-heading">
-                    <strong>Secret Assignment</strong>
-                </div>
-                <div class="panel-body">
-                    {% with vm_tab_active=form.initial.virtual_machine %}
-                        <ul class="nav nav-tabs" role="tablist">
-                            <li role="presentation"{% if not vm_tab_active %} class="active"{% endif %}><a href="#device" role="tab" data-toggle="tab">Device</a></li>
-                            <li role="presentation"{% if vm_tab_active %} class="active"{% endif %}><a href="#virtualmachine" role="tab" data-toggle="tab">Virtual Machine</a></li>
-                        </ul>
-                        <div class="tab-content">
-                            <div class="tab-pane{% if not vm_tab_active %} active{% endif %}" id="device">
-                                {% render_field form.device %}
-                            </div>
-                            <div class="tab-pane{% if vm_tab_active %} active{% endif %}" id="virtualmachine">
-                                {% render_field form.virtual_machine %}
-                            </div>
-                        </div>
-                    {% endwith %}
-                    {% render_field form.role %}
-                    {% render_field form.name %}
-                    {% render_field form.userkeys %}
-                    {% render_field form.tags %}
-                </div>
+{% block title %}{% if obj.pk %}Editing {{ obj }}{% else %}Add a Secret{% endif %}{% endblock %}
+
+{% block form %}
+{% render_errors form %}
+
+    
+{{ form.private_key }}
+<div class="field-group">
+    <h4>Secret Assignment</h4>
+    <ul class="nav nav-tabs mb-3" role="tablist">
+    <li class="nav-item" role="presentation">
+        <button
+            role="tab"
+            type="button"
+            id="device_tab"
+            data-bs-toggle="tab"
+            class="nav-link{% if not vm_tab_active %} active{% endif %}"
+            data-bs-target="#device"
+            aria-controls="device"
+        >
+            Device
+        </button>
+    </li>
+    <li class="nav-item" role="presentation">
+        <button
+            role="tab"
+            type="button"
+            id="vm_tab"
+            data-bs-toggle="tab"
+            class="nav-link{% if vm_tab_active %} active{% endif %}"
+            data-bs-target="#virtualmachine"
+            aria-controls="virtualmachine"
+        >
+            Virtual Machine
+        </button>
+    </li>
+    </ul>
+    {% with vm_tab_active=form.initial.virtual_machine %}
+        <div class="tab-content">
+            <div class="tab-pane{% if not vm_tab_active %} active{% endif %}" id="device">
+                {% render_field form.device %}
             </div>
-            <div class="panel panel-default">
-                <div class="panel-heading"><strong>Secret Data</strong></div>
-                <div class="panel-body">
-                    {% if obj.pk %}
-                        <div class="form-group">
-                            <label class="col-md-3 control-label required">Current Plaintext</label>
-                            <div class="col-md-7">
-                                <p class="form-control-static" id="secret_{{ obj.pk }}">********</p>
-                            </div>
-                            <div class="col-md-2 text-right">
-                                <button class="btn btn-xs btn-success unlock-secret" secret-id="{{ obj.pk }}">
-                                    <i class="mdi mdi-lock"></i> Unlock
-                                </button>
-                                <button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ obj.pk }}">
-                                    <i class="mdi mdi-lock-open"></i> Lock
-                                </button>
-                            </div>
-                        </div>
-                    {% endif %}
-                    {% render_field form.plaintext %}
-                    {% render_field form.plaintext2 %}
-                </div>
+            <div class="tab-pane{% if vm_tab_active %} active{% endif %}" id="virtualmachine">
+                {% render_field form.virtual_machine %}
             </div>
-            {% if form.custom_fields %}
-                <div class="panel panel-default">
-                    <div class="panel-heading"><strong>Custom Fields</strong></div>
-                    <div class="panel-body">
-                        {% render_custom_fields form %}
-                    </div>
-                </div>
-            {% endif %}
         </div>
-    </div>
-    <div class="row">
-        <div class="form-group">
-            <div class="col-md-12 text-center">
-                {% if obj.pk %}
-                    <button type="submit" name="_update" class="btn btn-primary">Update</button>
-                    <a href="{% url 'secrets:secret' pk=obj.pk %}" class="btn btn-default">Cancel</a>
-                {% else %}
-                    <button type="submit" name="_create" class="btn btn-primary">Create</button>
-                    <button type="submit" name="_addanother" class="btn btn-primary">Create and Add Another</button>
-                    <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
-                {% endif %}
-		    </div>
+    {% endwith %}
+    {% render_field form.role %}
+    {% render_field form.name %}
+    {% render_field form.userkeys %}
+    {% render_field form.tags %}
+</div>
+<div class="field-group">
+    <h4>Secret Data</h4>
+    {% if obj.pk %}
+        <div class="form-floating mb-3">
+            <input class="form-control" value="********" id="secret_{{ obj.pk }}" />
+            <label class="required">Current Plain Text</label>
+        </div>
+        <div class="col-md-2 text-end">
+            <button class="btn btn-sm btn-success unlock-secret" data-secret-id="{{ obj.pk }}">
+                <i class="mdi mdi-lock"></i> Unlock
+            </button>
+            <button class="btn btn-sm, btn-danger lock-secret collapse" data-secret-id="{{ obj.pk }}">
+                <i class="mdi mdi-lock-open"></i> Lock
+            </button>
+        </div>
+    {% endif %}
+    {% render_field form.plaintext %}
+    {% render_field form.plaintext2 %}
+
+</div>
+{% if form.custom_fields %}
+    <div class="card">
+        <h5 class="card-header">Custom Fields</h5>
+        <div class="card-body">
+            {% render_custom_fields form %}
         </div>
     </div>
-</form>
+{% endif %}
 
 {% include 'secrets/inc/private_key_modal.html' %}
 {% endblock %}
 
-{% block javascript %}
-<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
-{% endblock %}

+ 20 - 22
netbox/templates/users/api_tokens.html

@@ -7,15 +7,15 @@
     <div class="row">
         <div class="col-md-12">
             {% for token in tokens %}
-                <div class="panel panel-{% if token.is_expired %}danger{% else %}default{% endif %}">
-                    <div class="panel-heading">
-                        <div class="pull-right noprint">
-                            <a class="btn btn-xs btn-success copy-token" data-clipboard-target="#token_{{ token.pk }}">Copy</a>
+                <div class="card{% if token.is_expired %} bg-danger{% endif %}">
+                    <div class="card-header">
+                        <div class="float-end noprint">
+                            <a class="m-1 btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ token.pk }}">Copy</a>
                             {% if perms.users.change_token %}
-                                <a href="{% url 'user:token_edit' pk=token.pk %}" class="btn btn-xs btn-warning">Edit</a>
+                                <a href="{% url 'user:token_edit' pk=token.pk %}" class="m-1 btn btn-sm btn-warning">Edit</a>
                             {% endif %}
                             {% if perms.users.delete_token %}
-                                <a href="{% url 'user:token_delete' pk=token.pk %}" class="btn btn-xs btn-danger">Delete</a>
+                                <a href="{% url 'user:token_delete' pk=token.pk %}" class="m-1 btn btn-sm btn-danger">Delete</a>
                             {% endif %}
                         </div>
                         <i class="mdi mdi-key"></i>
@@ -24,7 +24,7 @@
                             <span class="label label-danger">Expired</span>
                         {% endif %}
                     </div>
-                    <div class="panel-body">
+                    <div class="card-body">
                         <div class="row">
                             <div class="col-md-4">
                                 <small class="text-muted">Created</small><br />
@@ -55,22 +55,20 @@
             {% empty %}
                 <p>You do not have any API tokens.</p>
             {% endfor %}
-            {% if perms.users.add_token %}
-                <a href="{% url 'user:token_add' %}" class="btn btn-primary">
-                    <span class="mdi mdi-plus-thick" aria-hidden="true"></span>
-                    Add a token
-                </a>
-            {% else %}
-                <div class="alert alert-info text-center" role="alert">
-                    You do not have permission to create new API tokens. If needed, ask an administrator to enable token creation for your account or an assigned group.
+            <div class="row my-3">
+                <div class="col-md-12">
+                {% if perms.users.add_token %}
+                    <a href="{% url 'user:token_add' %}" class="btn btn-primary">
+                        <span class="mdi mdi-plus-thick" aria-hidden="true"></span>
+                        Add a Token
+                    </a>
+                {% else %}
+                    <div class="alert alert-info text-center" role="alert">
+                        You do not have permission to create new API tokens. If needed, ask an administrator to enable token creation for your account or an assigned group.
+                    </div>
+                {% endif %}
                 </div>
-            {% endif %}
+            </div>
         </div>
     </div>
 {% endblock %}
-
-{% block javascript %}
-<script type="text/javascript">
-new ClipboardJS('.copy-token');
-</script>
-{% endblock %}

+ 12 - 25
netbox/templates/users/base.html

@@ -1,34 +1,21 @@
-{% extends 'base.html' %}
+{% extends 'layout.html' %}
+
+{% block title %}{% endblock %}
 
 {% block content %}
 <div class="row">
-    <div class="col-sm-12 col-md-10 col-md-offset-1">
-        <h1>{% block title %}{% endblock %}</h1>
-    </div>
-</div>
-<div class="row">
-    <div class="col-sm-3 col-md-2 col-md-offset-1">
-        <ul class="nav nav-pills nav-stacked">
-            <li{% if active_tab == "profile" %} class="active"{% endif %}>
-                <a href="{% url 'user:profile' %}">Profile</a>
-            </li>
-            <li{% if active_tab == "preferences" %} class="active"{% endif %}>
-                <a href="{% url 'user:preferences' %}">Preferences</a>
-            </li>
+    <div class="col-sm-3 col-md-2 col-md-offset-1 border-end">
+        <nav class="nav nav-pills nav-justified flex-column">
+            <a class="nav-item nav-link text-start{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'user:profile' %}">Profile</a>
+            <a class="nav-link nav-item text-start{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'user:preferences' %}">Preferences</a>
             {% if not request.user.ldap_username %}
-                <li{% if active_tab == "change_password" %} class="active"{% endif %}>
-                    <a href="{% url 'user:change_password' %}">Change Password</a>
-                </li>
+            <a class="nav-link nav-item text-start{% if active_tab == 'change-password' %} active{% endif %}" href="{% url 'user:change_password' %}">Change Password</a>
             {% endif %}
-            <li{% if active_tab == "api_tokens" %} class="active"{% endif %}>
-                <a href="{% url 'user:token_list' %}">API Tokens</a>
-            </li>
-            <li{% if active_tab == "userkey" %} class="active"{% endif %}>
-                <a href="{% url 'user:userkey' %}">User Key</a>
-            </li>
-        </ul>
+            <a class="nav-link nav-item text-start{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'user:token_list' %}">API Tokens</a>
+            <a class="nav-link nav-item text-start{% if active_tab == 'userkey' %} active{% endif %}" href="{% url 'user:userkey' %}">User Key</a>
+        </nav>
     </div>
-	<div class="col-sm-9 col-md-8">
+	<div class="col-sm-9 col-md-8 px-4">
         {% block usercontent %}{% endblock %}
 	</div>
 </div>

+ 9 - 17
netbox/templates/users/change_password.html

@@ -3,28 +3,20 @@
 
 {% block title %}Change Password{% endblock %}
 
+{% render_errors form %}
+
 {% block usercontent %}
     <form action="." method="post" class="form form-horizontal col-md-10 col-md-offset-1">
         {% csrf_token %}
-        {% if form.non_field_errors %}
-            <div class="panel panel-danger">
-                <div class="panel-heading"><strong>Errors</strong></div>
-                <div class="panel-body">
-                    {{ form.non_field_errors }}
-                </div>
-            </div>
-        {% endif %}
-        <div class="panel panel-default">
-            <div class="panel-heading"><strong>Password</strong></div>
-            <div class="panel-body">
-                {% render_field form.old_password %}
-                {% render_field form.new_password1 %}
-                {% render_field form.new_password2 %}
-            </div>
+        <div class="field-group">
+            <h4>Password</h4>
+            {% render_field form.old_password %}
+            {% render_field form.new_password1 %}
+            {% render_field form.new_password2 %}
         </div>
-        <div class="text-right">
+        <div class="text-end">
+            <a href="{% url 'user:profile' %}" class="btn btn-outline-danger">Cancel</a>
             <button type="submit" name="_update" class="btn btn-primary">Update</button>
-            <a href="{% url 'user:profile' %}" class="btn btn-default">Cancel</a>
         </div>
     </form>
 {% endblock %}

+ 23 - 5
netbox/templates/users/profile.html

@@ -4,16 +4,34 @@
 {% block title %}User Profile{% endblock %}
 
 {% block usercontent %}
-    <small class="text-muted">User login</small>
+    <small class="text-muted">User Login</small>
     <h5>{{ request.user.username }}</h5>
-    <small class="text-muted">Full name</small>
-    <h5>{{ request.user.first_name }} {{ request.user.last_name }}</h5>
+    <small class="text-muted">Full Name</small>
+    <h5>
+    {% if request.user.first_name and request.user.last_name %}
+    {{ request.user.first_name }} {{ request.user.last_name }}
+    {% elif request.user.first_name and not request.user.last_name %}
+    {{ request.user.first_name }}
+    {% elif request.user.last_name and not request.user.first_name %}
+    {{ request.user.last_name }}
+    {% else %}
+    <span class="text-muted">None</span>
+    {% endif %}
+    </h5>
     <small class="text-muted">Email</small>
     <h5>{{ request.user.email }}</h5>
     <small class="text-muted">Registered</small>
     <h5>{{ request.user.date_joined }}</h5>
     <small class="text-muted">Groups</small>
-    <h5>{{ request.user.groups.all|join:', ' }}</h5>
-    <small class="text-muted">Admin access</small>
+    <h5>
+        {% if request.user.groups.all %}
+            {% for group in request.user.groups.all %}
+                <span class="badge bg-secondary">{{ group }}</span>
+            {% endfor %}
+        {% else %}
+            <span class="text-muted">None</span>
+        {% endif %}
+    </h5>
+    <small class="text-muted">Admin Access</small>
     <h5>{{ request.user.is_staff|yesno|capfirst }}</h5>
 {% endblock %}

+ 6 - 6
netbox/templates/users/userkey.html

@@ -4,18 +4,18 @@
 
 {% block usercontent %}
     {% if object %}
-        <div class="pull-right noprint">
+        <div class="float-end noprint">
             <a href="{% url 'user:userkey_edit' %}" class="btn btn-warning">
                 <span class="mdi mdi-pencil" aria-hidden="true"></span>
-                Edit user key
+                Edit User Key
             </a>
         </div>
         <h4>
-            Your user key is:
+            Your user key is 
             {% if object.is_active %}
-                <span class="label label-success">Active</span>
+                <span class="badge bg-success">Active</span>
             {% else %}
-                <span class="label label-danger">Inactive</span>
+                <span class="badge bg-danger">Inactive</span>
             {% endif %}
         </h4>
         {% include 'inc/created_updated.html' %}
@@ -25,7 +25,7 @@
                 Your user key is inactive. Ask an administrator to enable it for you.
             </div>
         {% endif %}
-        <pre>{{ object.public_key }}</pre>
+        <pre class="border rounded  p-3 copyable">{{ object.public_key }}</pre>
         <hr />
         {% if object.session_key %}
             <div class="pull-right noprint">

+ 18 - 23
netbox/templates/users/userkey_edit.html

@@ -13,48 +13,43 @@
     {% endif %}
     <form action="." method="post" class="form">
         {% csrf_token %}
-        <div class="form-group">
+        <div class="field-group">
             {% render_field form.public_key %}
         </div>
-        <div class="row">
-            <div class="form-group">
-                <div class="col-sm-6 col-md-6">
-                    <button type="button" class="btn btn-info" id="generate_keypair">Generate a New Key Pair</button>
-                </div>
-                <div class="col-sm-6 col-md-6 text-right">
-                    <button type="submit" name="_update" class="btn btn-primary">Save</button>
-                    <a href="{% url 'user:userkey' %}" class="btn btn-default">Cancel</a>
-                </div>
+        <div class="row my-3">
+            <div class="col-4 text-start">
+                <button type="button" class="btn btn-info" id="generate_keypair" data-bs-toggle="modal" data-bs-target="#new_keypair_modal">Generate a New Key Pair</button>
+            </div>
+            <div class="col-8 text-end">
+                <a href="{% url 'user:userkey' %}" class="btn btn-outline-danger">Cancel</a>
+                <button type="submit" name="_update" class="btn btn-primary">Save</button>    
             </div>
         </div>
     </form>
     <div class="modal fade" id="new_keypair_modal" tabindex="-1" role="dialog">
-        <div class="modal-dialog modal-md" role="document">
+        <div class="modal-dialog modal-lg" role="document">
             <div class="modal-content">
                 <div class="modal-header">
-                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                     <h4 class="modal-title" id="new_keypair_modal_title">
                         New RSA Key Pair
                     </h4>
+                    <button type="button" class="btn btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                 </div>
                 <div class="modal-body">
-                    <strong>New Public Key</strong>
-                    <div class="form-group">
-                        <textarea class="form-control" id="new_pubkey" style="height: 250px;"></textarea>
+                    <div class="field-group">
+                        <h5>New Public Key</h5>
+                        <textarea class="form-control" rows="10"  id="new_pubkey" style="height: 250px;font-family:var(--bs-font-monospace);"></textarea>
                     </div>
-                    <strong>New Private Key</strong>
-                    <div class="form-group">
-                        <textarea class="form-control" id="new_privkey" style="height: 250px;"></textarea>
+                    
+                    <div class="field-group">
+                        <h5>New Private Key</h5>
+                        <textarea class="form-control" rows="10" id="new_privkey" style="height: 250px;font-family:var(--bs-font-monospace);"></textarea>
                     </div>
                 </div>
                 <div class="modal-footer text-center">
-                    <button type="button" class="btn btn-danger" id="use_new_pubkey" data-dismiss="modal">I have saved my new private key</button>
+                    <button type="button" class="btn btn-danger" id="use_new_pubkey" data-bs-dismiss="modal">I Saved My New Private Key</button>
                 </div>
             </div>
         </div>
     </div>
 {% endblock %}
-
-{% block javascript %}
-<script src="{% static 'js/secrets.js' %}?v{{ settings.VERSION }}"></script>
-{% endblock %}

+ 3 - 3
netbox/templates/utilities/render_errors.html

@@ -4,11 +4,11 @@
 {% if form.errors or form.non_field_errors %}
 <div class="alert alert-danger mt-3" role="alert">
   <h4 class="alert-heading">Errors</h4>
-  {% if form.errors %}
-  <hr />
+  {% if form.errors and '__all__' not in form.errors %}
+    <hr />
   {% endif %}
   <div class="ps-2">
-    {% if form.errors %}
+    {% if form.errors and '__all__' not in form.errors %}
     {% for field_name, errors in form.errors.items %}
     {% if not field_name|startswith:'__' %}
       {% with field=form|getfield:field_name %}

+ 75 - 73
netbox/templates/virtualization/cluster.html

@@ -5,72 +5,74 @@
 {% load plugins %}
 
 {% block breadcrumbs %}
-  <li><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></li>
+  <li class="breadcrumb-item"><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></li>
   {% if object.group %}
-    <li><a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a></li>
+    <li class="breadcrumb-item"><a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a></li>
   {% endif %}
-  <li>{{ object }}</li>
+  <li class="breadcrumb-item">{{ object }}</li>
 {% endblock %}
 
 {% block content %}
 <div class="row">
 	<div class="col-md-5">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Cluster</strong>
+        <div class="card">
+            <h5 class="card-header">
+                Cluster
+            </h5>
+            <div class="card-body">
+                <table class="table table-hover attr-table">
+                    <tr>
+                        <th scope="row">Name</th>
+                        <td>{{ object.name }}</td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Type</th>
+                        <td><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Group</th>
+                        <td>
+                            {% if object.group %}
+                                <a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
+                            {% else %}
+                                <span class="text-muted">None</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Tenant</th>
+                        <td>
+                            {% if object.tenant %}
+                                <a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
+                            {% else %}
+                                <span class="text-muted">None</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Site</th>
+                        <td>
+                            {% if object.site %}
+                                <a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
+                            {% else %}
+                                <span class="text-muted">None</span>
+                            {% endif %}
+                        </td>
+                    </tr>
+                    <tr>
+                        <th scope="row">Virtual Machines</th>
+                        <td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ object.pk }}">{{ object.virtual_machines.count }}</a></td>
+                    </tr>
+                </table>
             </div>
-            <table class="table table-hover panel-body attr-table">
-                <tr>
-                    <td>Name</td>
-                    <td>{{ object.name }}</td>
-                </tr>
-                <tr>
-                    <td>Type</td>
-                    <td><a href="{{ object.type.get_absolute_url }}">{{ object.type }}</a></td>
-                </tr>
-                <tr>
-                    <td>Group</td>
-                    <td>
-                        {% if object.group %}
-                            <a href="{{ object.group.get_absolute_url }}">{{ object.group }}</a>
-                        {% else %}
-                            <span class="text-muted">None</span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Tenant</td>
-                    <td>
-                        {% if object.tenant %}
-                            <a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
-                        {% else %}
-                            <span class="text-muted">None</span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Site</td>
-                    <td>
-                        {% if object.site %}
-                            <a href="{{ object.site.get_absolute_url }}">{{ object.site }}</a>
-                        {% else %}
-                            <span class="text-muted">None</span>
-                        {% endif %}
-                    </td>
-                </tr>
-                <tr>
-                    <td>Virtual Machines</td>
-                    <td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ object.pk }}">{{ object.virtual_machines.count }}</a></td>
-                </tr>
-            </table>
         </div>
         {% include 'inc/custom_fields_panel.html' %}
         {% include 'extras/inc/tags_panel.html' with tags=object.tags.all url='virtualization:cluster_list' %}
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Comments</strong>
-            </div>
-            <div class="panel-body rendered-markdown">
+        <div class="card">
+            <h5 class="card-header">
+                Comments
+            </h5>
+            <div class="card-body rendered-markdown">
                 {% if object.comments %}
                     {{ object.comments|render_markdown }}
                 {% else %}
@@ -81,30 +83,30 @@
         {% plugin_left_page object %}
     </div>
     <div class="col-md-7">
-        <div class="panel panel-default">
-            <div class="panel-heading">
-                <strong>Host Devices</strong>
-            </div>
-            {% if perms.virtualization.change_cluster %}
+        <div class="card">
+            <h5 class="card-header">
+                Host Devices
+            </h5>
+            <div class="card-body">
+                {% if perms.virtualization.change_cluster %}
                 <form action="{% url 'virtualization:cluster_remove_devices' pk=object.pk %}" method="post">
-                {% csrf_token %}
-            {% endif %}
-            {% include 'responsive_table.html' with table=device_table %}
-            {% if perms.virtualization.change_cluster %}
-                <div class="panel-footer noprint">
-                    <div class="pull-right">
-                        <a href="{% url 'virtualization:cluster_add_devices' pk=object.pk %}?site={{ object.site.pk }}" class="btn btn-primary btn-xs">
+                    {% csrf_token %}
+                    {% include 'responsive_table.html' with table=device_table %}
+                    {% if perms.virtualization.change_cluster %}
+                    <div class="card-footer noprint justify-content-between d-flex">
+                        <button type="submit" name="_remove" class="btn btn-danger primary btn-sm">
+                            <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
+                            Remove devices
+                        </button>
+                        <a href="{% url 'virtualization:cluster_add_devices' pk=object.pk %}?site={{ object.site.pk }}" class="btn btn-primary btn-sm">
                             <span class="mdi mdi-plus-thick" aria-hidden="true"></span>
                             Add devices
                         </a>
                     </div>
-                    <button type="submit" name="_remove" class="btn btn-danger primary btn-xs">
-                        <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
-                        Remove devices
-                    </button>
-                </div>
                 </form>
-            {% endif %}
+                {% endif %}
+                {% endif %}
+            </div>
         </div>
         {% plugin_right_page object %}
 	</div>

+ 11 - 18
netbox/templates/virtualization/cluster_add_devices.html

@@ -1,36 +1,29 @@
-{% extends 'base.html' %}
+{% extends 'generic/object_edit.html' %}
 {% load static %}
 {% load form_helpers %}
 
+{% render_errors form %}
+
+{% block title %}Add Device to Cluster {{ cluster }}{% endblock %}
+
 {% block content %}
-    <form action="." method="post" class="form form-horizontal">
+    <form action="." method="post">
         {% csrf_token %}
         {% for field in form.hidden_fields %}
             {{ field }}
         {% endfor %}
         <div class="row">
             <div class="col-md-6 col-md-offset-3">
-                <h3>{% block title %}Add Devices to Cluster {{ cluster }}{% endblock %}</h3>
-                {% if form.non_field_errors %}
-                    <div class="panel panel-danger">
-                        <div class="panel-heading"><strong>Errors</strong></div>
-                        <div class="panel-body">
-                            {{ form.non_field_errors }}
-                        </div>
-                    </div>
-                {% endif %}
-                <div class="panel panel-default">
-                    <div class="panel-heading"><strong>Device Selection</strong></div>
-                    <div class="panel-body">
-                        {% render_form form %}
-                    </div>
+                <div class="field-group">
+                    <h4>Device Selection</h4>
+                    {% render_form form %}
                 </div>
             </div>
         </div>
         <div class="row">
-            <div class="col-md-6 col-md-offset-3 text-right noprint">
+            <div class="col-md-6 col-md-offset-3 text-end noprint">
+                <a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
                 <button type="submit" name="_add" class="btn btn-primary">Add Devices</button>
-                <a href="{{ return_url }}" class="btn btn-default">Cancel</a>
             </div>
         </div>
     </form>

+ 28 - 32
netbox/templates/virtualization/vminterface_edit.html

@@ -2,41 +2,37 @@
 {% load form_helpers %}
 
 {% block form %}
-    <div class="panel panel-default">
-        <div class="panel-heading"><strong>Interface</strong></div>
-        <div class="panel-body">
-            {% if form.instance.virtual_machine %}
-                <div class="form-group">
-                    <label class="col-md-3 control-label required" for="id_device">Virtual Machine</label>
-                    <div class="col-md-9">
-                        <p class="form-control-static">
-                            <a href="{{ form.instance.virtual_machine.get_absolute_url }}">{{ form.instance.virtual_machine }}</a>
-                        </p>
-                    </div>
-                </div>
-            {% endif %}
-            {% render_field form.name %}
-            {% render_field form.enabled %}
-            {% render_field form.mac_address %}
-            {% render_field form.mtu %}
-            {% render_field form.description %}
-            {% render_field form.tags %}
-        </div>
+    <div class="field-group">
+        <h4>Interface</h4>
+        {% if form.instance.virtual_machine %}
+            <div class="form-floating">
+                <input class="form-control" value="{{ form.instance.virtual_machine }}"/>
+                {% comment %} <div class="col-md-9">
+                    <p class="form-control-static">
+                        <a href="{{ form.instance.virtual_machine.get_absolute_url }}">{{ form.instance.virtual_machine }}</a>
+                    </p>
+                </div> {% endcomment %}
+                <label class="col-md-3 control-label required" for="id_device">Virtual Machine</label>
+            </div>
+        {% endif %}
+        {% render_field form.name %}
+        {% render_field form.enabled %}
+        {% render_field form.mac_address %}
+        {% render_field form.mtu %}
+        {% render_field form.description %}
+        {% render_field form.tags %}
+        
     </div>
-    <div class="panel panel-default">
-        <div class="panel-heading"><strong>802.1Q Switching</strong></div>
-        <div class="panel-body">
-            {% render_field form.mode %}
-            {% render_field form.untagged_vlan %}
-            {% render_field form.tagged_vlans %}
-        </div>
+    <div class="field-group">
+        <h4>802.1Q Switching</h4>
+        {% render_field form.mode %}
+        {% render_field form.untagged_vlan %}
+        {% render_field form.tagged_vlans %}
     </div>
     {% if form.custom_fields %}
-      <div class="panel panel-default">
-        <div class="panel-heading"><strong>Custom Fields</strong></div>
-        <div class="panel-body">
-          {% render_custom_fields form %}
-        </div>
+      <div class="field-group">
+        <h4>Custom Fields</h4>
+        {% render_custom_fields form %}
       </div>
     {% endif %}
 {% endblock %}