checktheroads 4 лет назад
Родитель
Сommit
2b159fc40f
55 измененных файлов с 1212 добавлено и 345 удалено
  1. 1 0
      netbox/netbox/context_processors.py
  2. 1 0
      netbox/project-static/.prettierignore
  3. 2 2
      netbox/project-static/README.md
  4. 55 0
      netbox/project-static/bundle.js
  5. 0 44
      netbox/project-static/choices.scss
  6. BIN
      netbox/project-static/dist/bootstrap-icons.3c6f7aa7.woff2
  7. BIN
      netbox/project-static/dist/bootstrap-icons.9171de62.woff
  8. 20 23
      netbox/project-static/dist/config.js
  9. 0 0
      netbox/project-static/dist/config.js.map
  10. 20 23
      netbox/project-static/dist/jobs.js
  11. 0 0
      netbox/project-static/dist/jobs.js.map
  12. 20 23
      netbox/project-static/dist/lldp.js
  13. 0 0
      netbox/project-static/dist/lldp.js.map
  14. 0 0
      netbox/project-static/dist/materialdesignicons-webfont.1f34ab41.woff
  15. 0 0
      netbox/project-static/dist/materialdesignicons-webfont.23408420.ttf
  16. BIN
      netbox/project-static/dist/materialdesignicons-webfont.2fc9ee7e.ttf
  17. 0 0
      netbox/project-static/dist/materialdesignicons-webfont.6dab8170.woff2
  18. 0 0
      netbox/project-static/dist/materialdesignicons-webfont.6e1fcca2.eot
  19. BIN
      netbox/project-static/dist/materialdesignicons-webfont.a4523412.woff2
  20. BIN
      netbox/project-static/dist/materialdesignicons-webfont.b5feabcf.eot
  21. BIN
      netbox/project-static/dist/materialdesignicons-webfont.c803ff2e.woff
  22. BIN
      netbox/project-static/dist/materialdesignicons-webfont.e8effb94.woff2
  23. 0 0
      netbox/project-static/dist/netbox.css
  24. 0 0
      netbox/project-static/dist/netbox.css.map
  25. 50 50
      netbox/project-static/dist/netbox.js
  26. 0 0
      netbox/project-static/dist/netbox.js.map
  27. 1 1
      netbox/project-static/dist/rack_elevation.css
  28. 0 0
      netbox/project-static/dist/rack_elevation.css.map
  29. 1 1
      netbox/project-static/dist/status.js
  30. 0 0
      netbox/project-static/dist/status.js.map
  31. BIN
      netbox/project-static/dist/ui-icons_444444_256x240.9d73b7df.png
  32. BIN
      netbox/project-static/dist/ui-icons_555555_256x240.c391669a.png
  33. BIN
      netbox/project-static/dist/ui-icons_777620_256x240.558700ce.png
  34. BIN
      netbox/project-static/dist/ui-icons_777777_256x240.8ad0c003.png
  35. BIN
      netbox/project-static/dist/ui-icons_cc0000_256x240.5fc9b083.png
  36. BIN
      netbox/project-static/dist/ui-icons_ffffff_256x240.ef9e8fee.png
  37. 15 3
      netbox/project-static/main.scss
  38. 173 58
      netbox/project-static/netbox.scss
  39. 1 3
      netbox/project-static/package.json
  40. 90 56
      netbox/project-static/rack_elevation.scss
  41. 13 0
      netbox/project-static/select.scss
  42. 134 0
      netbox/project-static/src/colorMode.ts
  43. 2 1
      netbox/project-static/src/netbox.ts
  44. 211 0
      netbox/project-static/theme-base.scss
  45. 292 0
      netbox/project-static/theme-dark.scss
  46. 6 14
      netbox/project-static/theme-light.scss
  47. 1 1
      netbox/project-static/tsconfig.json
  48. 5 1
      netbox/templates/base.html
  49. 1 1
      netbox/templates/changelog.html
  50. 1 1
      netbox/templates/inc/search_panel.html
  51. 3 3
      netbox/templates/layout.html
  52. 18 0
      netbox/templates/logo.html
  53. 12 6
      netbox/templates/profile_button.html
  54. 51 27
      netbox/templates/users/preferences.html
  55. 12 3
      netbox/users/views.py

+ 1 - 0
netbox/netbox/context_processors.py

@@ -10,4 +10,5 @@ def settings_and_registry(request):
     return {
     return {
         'settings': django_settings,
         'settings': django_settings,
         'registry': registry,
         'registry': registry,
+        'preferences': request.user.config,
     }
     }

+ 1 - 0
netbox/project-static/.prettierignore

@@ -0,0 +1 @@
+dist

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

@@ -65,14 +65,14 @@ To bundle only CSS files, run:
 
 
 ```bash
 ```bash
 # netbox/project-static
 # netbox/project-static
-yarn bundle:css
+yarn bundle --styles
 ```
 ```
 
 
 To bundle only JS files, run:
 To bundle only JS files, run:
 
 
 ```bash
 ```bash
 # netbox/project-static
 # netbox/project-static
-yarn bundle:js
+yarn bundle --scripts
 ```
 ```
 
 
 Or, to bundle both, run:
 Or, to bundle both, run:

+ 55 - 0
netbox/project-static/bundle.js

@@ -0,0 +1,55 @@
+const Bundler = require('parcel-bundler');
+
+const options = {
+  watch: false,
+  minify: true,
+  outDir: './dist',
+  publicUrl: '/static',
+  logLevel: 2,
+  cache: true,
+};
+
+const args = process.argv.slice(2);
+
+if (args.includes('--no-cache')) {
+  options.cache = false;
+}
+
+const styles = [
+  ['main.scss', 'netbox.css'],
+  ['rack_elevation.scss', 'rack_elevation.css'],
+];
+
+const scripts = [
+  ['src/index.ts', 'netbox.js'],
+  ['src/jobs.ts', 'jobs.js'],
+  ['src/device/lldp.ts', 'lldp.js'],
+  ['src/device/config.ts', 'config.js'],
+  ['src/device/status.ts', 'status.js'],
+];
+
+async function bundleStyles() {
+  for (const [input, outFile] of styles) {
+    const instance = new Bundler(input, { outFile, ...options });
+    await instance.bundle();
+  }
+}
+
+async function bundleScripts() {
+  for (const [input, outFile] of scripts) {
+    const instance = new Bundler(input, { outFile, ...options });
+    await instance.bundle();
+  }
+}
+
+async function bundleAll() {
+  if (args.includes('--styles')) {
+    return await bundleStyles();
+  } else if (args.includes('--scripts')) {
+    return await bundleScripts();
+  }
+  await bundleStyles();
+  await bundleScripts();
+}
+
+bundleAll();

+ 0 - 44
netbox/project-static/choices.scss

@@ -1,44 +0,0 @@
-$choices-font-size-lg: $form-select-font-size-lg;
-$choices-font-size-md: $form-select-font-size;
-$choices-font-size-sm: $form-select-font-size-sm;
-$choices-guttering: $form-select-padding-y;
-$choices-border-radius: $form-select-border-radius;
-$choices-bg-color: $form-select-bg;
-$choices-bg-color-disabled: $form-select-disabled-bg;
-$choices-bg-color-dropdown: $form-select-bg;
-$choices-text-color: $form-select-color;
-$choices-keyline-color: $form-select-border-color;
-$choices-primary-color: $primary;
-$choices-disabled-color: $form-select-disabled-color;
-$choices-highlight-color: $choices-primary-color;
-$choices-button-dimension: $form-select-bg-size;
-
-.choices {
-  .choices__list--dropdown .choices__item--selectable.is-highlighted[class] {
-    background-color: $primary;
-    color: white;
-  }
-
-  // Floating-input adjusts the z-index of the label. This fixes an issue where if there are two
-  // floating inputs on top of eachother, the label of an overlapping field is visible inside the
-  // dropdown's dropdown list.
-  &.is-open .choices__list--dropdown {
-    z-index: 10;
-  }
-}
-
-.choices[data-type*='select-one'] select.choices__input {
-  display: block !important;
-  opacity: 0;
-  pointer-events: none;
-  position: absolute;
-  left: 0;
-  bottom: 0;
-}
-
-select[data-ssid] {
-  display: block !important;
-  opacity: 0;
-  pointer-events: none;
-  position: absolute;
-}

BIN
netbox/project-static/dist/bootstrap-icons.3c6f7aa7.woff2


BIN
netbox/project-static/dist/bootstrap-icons.9171de62.woff


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


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


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


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


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


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


+ 0 - 0
netbox/project-static/dist/materialdesignicons-webfont.b58ee716.woff → netbox/project-static/dist/materialdesignicons-webfont.1f34ab41.woff


+ 0 - 0
netbox/project-static/dist/materialdesignicons-webfont.ec4cf2c3.ttf → netbox/project-static/dist/materialdesignicons-webfont.23408420.ttf


BIN
netbox/project-static/dist/materialdesignicons-webfont.a58ac4bb.eot → netbox/project-static/dist/materialdesignicons-webfont.2fc9ee7e.ttf


+ 0 - 0
netbox/project-static/dist/materialdesignicons-webfont.ee0e0f45.woff2 → netbox/project-static/dist/materialdesignicons-webfont.6dab8170.woff2


+ 0 - 0
netbox/project-static/dist/materialdesignicons-webfont.e54fd8da.eot → netbox/project-static/dist/materialdesignicons-webfont.6e1fcca2.eot


BIN
netbox/project-static/dist/materialdesignicons-webfont.a4523412.woff2


BIN
netbox/project-static/dist/materialdesignicons-webfont.67000b74.ttf → netbox/project-static/dist/materialdesignicons-webfont.b5feabcf.eot


BIN
netbox/project-static/dist/materialdesignicons-webfont.82133573.woff → netbox/project-static/dist/materialdesignicons-webfont.c803ff2e.woff


BIN
netbox/project-static/dist/materialdesignicons-webfont.e8effb94.woff2


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


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


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


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


+ 1 - 1
netbox/project-static/dist/rack_elevation.css

@@ -1,2 +1,2 @@
-*{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:.875rem}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}.rack{background-color:#f8f9fa;fill:none;stroke:#000;stroke-width:2px}.slot{fill:#e9ecef;stroke:#adb5bd}.slot:hover{fill:#fff}.slot+.add-device{fill:none}.add-device:hover,.slot:hover+.add-device{fill:#0d6efd}.add-device:hover+.slot{fill:#fff}.reserved,.reserved:hover{fill:url(#reserved)}.occupied,.occupied:hover{fill:url(#occupied)}.blocked,.blocked:hover{fill:url(#blocked)}.blocked:hover+.add-device{fill:none}.unit{margin:0;padding:5px 0;fill:#ced4da;font-size:.875rem;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.hidden{visibility:hidden}
+*{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:.875rem}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg .rack{background-color:#f3f4f6;fill:none;stroke:#111827;stroke-width:2px}svg .slot{fill:#f3f4f6;stroke:#6b7280}svg .slot:hover{fill:#f9fafb}svg .slot+.add-device{fill:none}svg .slot .add-device:hover,svg .slot:hover+.add-device{fill:#3b82f6}svg .slot .add-device:hover+.slot{fill:#fff}svg .slot.reserved:hover[class],svg .slot.reserved[class]{fill:url(#reserved)}svg .slot.occupied:hover[class],svg .slot.occupied[class]{fill:url(#occupied)}svg .slot.blocked:hover[class],svg .slot.blocked[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{fill:none}svg .unit{margin:0;padding:5px 0;fill:#9ca3af;font-size:.875rem;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}svg .hidden{visibility:hidden}svg[data-netbox-color-mode=dark] .rack{background-color:#1f2937}svg[data-netbox-color-mode=dark] .slot{fill:#374151;stroke:#9ca3af}svg[data-netbox-color-mode=dark] .slot:hover{fill:#4b5563}svg[data-netbox-color-mode=dark] .slot+.add-device{fill:none}svg[data-netbox-color-mode=dark] .add-device:hover,svg[data-netbox-color-mode=dark] .slot:hover+.add-device{fill:#93c5fd}svg[data-netbox-color-mode=dark] .add-device:hover+.slot{fill:#000}
 /*# sourceMappingURL=/static/rack_elevation.css.map */
 /*# sourceMappingURL=/static/rack_elevation.css.map */

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


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


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


BIN
netbox/project-static/dist/ui-icons_444444_256x240.9d73b7df.png


BIN
netbox/project-static/dist/ui-icons_555555_256x240.c391669a.png


BIN
netbox/project-static/dist/ui-icons_777620_256x240.558700ce.png


BIN
netbox/project-static/dist/ui-icons_777777_256x240.8ad0c003.png


BIN
netbox/project-static/dist/ui-icons_cc0000_256x240.5fc9b083.png


BIN
netbox/project-static/dist/ui-icons_ffffff_256x240.ef9e8fee.png


+ 15 - 3
netbox/project-static/main.scss

@@ -1,9 +1,21 @@
-@import './theme.scss';
+@import './theme-light.scss';
 @import './bootstrap.scss';
 @import './bootstrap.scss';
 
 
-@import './node_modules/@mdi/font/css/materialdesignicons.min.css';
+@import '@mdi/font/css/materialdesignicons.min.css';
 
 
 @import './select.scss';
 @import './select.scss';
-@import './node_modules/flatpickr/dist/flatpickr.min.css';
+@import 'flatpickr/dist/flatpickr.css';
 
 
 @import './netbox.scss';
 @import './netbox.scss';
+
+body[data-netbox-color-mode='dark'] {
+  @import './theme-dark.scss';
+  @import './bootstrap.scss';
+
+  @import '@mdi/font/css/materialdesignicons.min.css';
+
+  @import './select.scss';
+  @import 'flatpickr/dist/flatpickr.css';
+
+  @import './netbox.scss';
+}

+ 173 - 58
netbox/project-static/netbox.scss

@@ -1,3 +1,101 @@
+:root {
+  --nbx-logo-color-1: #9cc8f8;
+  --nbx-logo-color-2: #1685fc;
+  --nbx-sidebar-bg: #{$gray-100};
+  --nbx-sidebar-link-color: #{$gray-800};
+  --nbx-sidebar-link-hover-bg: #{$blue-100};
+  --nbx-sidebar-title-color: #{$text-muted};
+  --nbx-breadcrumb-bg: #{$light};
+  --nbx-body-bg: #{$white};
+  --nbx-body-color: #{$black};
+  --nbx-pre-bg: #{$gray-100};
+  --nbx-pre-border-color: #{$gray-600};
+  --nbx-change-added: #{rgba($green, 0.4)};
+  --nbx-change-removed: #{rgba($red, 0.4)};
+  --nbx-cable-node-bg: #{$gray-100};
+  --nbx-cable-node-border-color: #{$gray-200};
+  --nbx-cable-termination-bg: #{$gray-200};
+  --nbx-cable-termination-border-color: #{$gray-300};
+  --nbx-elevation-slot-bg: #{$gray-100};
+
+  body[data-netbox-color-mode='dark'] {
+    --nbx-logo-color-1: #{$white};
+    --nbx-logo-color-2: #{$gray-200};
+    --nbx-sidebar-bg: #{$gray-800};
+    --nbx-sidebar-link-color: #{$gray-200};
+    --nbx-sidebar-link-hover-bg: #{rgba($blue-300, 0.15)};
+    --nbx-sidebar-title-color: #{$gray-300};
+    --nbx-breadcrumb-bg: #{$gray-800};
+    --nbx-body-bg: #{$gray-900};
+    --nbx-body-color: #{$white};
+    --nbx-pre-bg: #{$gray-700};
+    --nbx-pre-border-color: #{$gray-600};
+    --nbx-change-added: #{rgba($green-300, 0.4)};
+    --nbx-change-removed: #{rgba($red-300, 0.4)};
+    --nbx-cable-node-bg: #{$gray-700};
+    --nbx-cable-node-border-color: #{$gray-600};
+    --nbx-cable-termination-bg: #{$gray-800};
+    --nbx-cable-termination-border-color: #{$gray-700};
+    --nbx-elevation-slot-bg: #{$gray-700};
+  }
+}
+
+* {
+  transition: background-color, color 0.15s ease-in-out;
+}
+
+body {
+  background-color: var(--nbx-body-bg);
+  color: var(--nbx-body-color);
+  g#netbox-logo-1 {
+    fill: #9cc8f8;
+    stroke: #9cc8f8;
+  }
+
+  g#netbox-logo-2 {
+    fill: #1685fc;
+    stroke: #1685fc;
+  }
+  &[data-netbox-color-mode='light'] {
+    .btn.btn-primary {
+      color: $white;
+    }
+  }
+  &[data-netbox-color-mode='dark'] {
+    a:not(.btn) {
+      color: $blue-300;
+    }
+    .breadcrumb .breadcrumb-item > a {
+      color: $blue-300;
+    }
+    .badge {
+      color: $black;
+    }
+    .card,
+    .sidebar {
+      .text-muted {
+        color: $gray-400 !important;
+      }
+    }
+    .text-body[class] {
+      color: var(--nbx-body-color) !important;
+    }
+    g#netbox-logo-1 {
+      fill: $white;
+      stroke: $white;
+    }
+
+    g#netbox-logo-2 {
+      fill: $gray-200;
+      stroke: $gray-200;
+    }
+  }
+}
+
+nav.search {
+  background-color: var(--nbx-body-bg);
+}
+
 main.login-container {
 main.login-container {
   display: flex;
   display: flex;
   height: calc(100vh - 4rem);
   height: calc(100vh - 4rem);
@@ -20,6 +118,37 @@ footer.login-footer {
   }
   }
 }
 }
 
 
+h1 {
+  font-weight: $font-weight-bolder;
+}
+
+h2 {
+  font-weight: $font-weight-bold;
+}
+
+h3,
+h4 {
+  font-weight: $font-weight-medium;
+}
+
+h5,
+h6 {
+  font-weight: $font-weight-medium;
+}
+
+h1.accordion-item-title,
+h2.accordion-item-title,
+h3.accordion-item-title,
+h4.accordion-item-title,
+h5.accordion-item-title,
+h6.accordion-item-title {
+  padding: 0 0.5rem;
+  font-weight: $font-weight-bold;
+  text-transform: uppercase;
+  color: var(--nbx-sidebar-title-color);
+  font-size: $font-size-sm;
+}
+
 .form-login {
 .form-login {
   width: 100%;
   width: 100%;
   max-width: 330px;
   max-width: 330px;
@@ -53,16 +182,6 @@ li.dropdown-item.dropdown-item-btns {
   align-items: center;
   align-items: center;
 }
 }
 
 
-.sidebar {
-  position: fixed;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  z-index: 100; /* Behind the navbar */
-  // padding: 48px 0 0; /* Height of navbar */
-  box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
-}
-
 @media (max-width: 767.98px) {
 @media (max-width: 767.98px) {
   .sidebar {
   .sidebar {
     top: 5rem;
     top: 5rem;
@@ -84,9 +203,28 @@ li.dropdown-item.dropdown-item-btns {
   font-size: 1rem;
   font-size: 1rem;
 }
 }
 
 
+nav.nav.nav-pills {
+  .nav-item.nav-link {
+    padding: 0.25rem 0.5rem;
+    font-size: $font-size-base;
+    border-radius: $border-radius;
+    &:hover {
+      color: $body-color;
+      background-color: var(--nbx-sidebar-link-hover-bg);
+    }
+  }
+}
+
 .sidebar {
 .sidebar {
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  z-index: 100; /* Behind the navbar */
+  box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
+  background-color: var(--nbx-sidebar-bg);
   .sidebar-nav-link {
   .sidebar-nav-link {
-    color: $gray-800;
+    color: var(--nbx-sidebar-link-color);
   }
   }
   .accordion-body {
   .accordion-body {
     max-height: calc(100vh - 24rem);
     max-height: calc(100vh - 24rem);
@@ -95,10 +233,11 @@ li.dropdown-item.dropdown-item-btns {
       .nav-link {
       .nav-link {
         padding: 0.25rem 0.5rem;
         padding: 0.25rem 0.5rem;
         font-size: $font-size-base;
         font-size: $font-size-base;
-      }
-      .nav-link:hover {
-        background-color: $blue-100;
         border-radius: $border-radius;
         border-radius: $border-radius;
+        &:hover {
+          color: $body-color;
+          background-color: var(--nbx-sidebar-link-hover-bg);
+        }
       }
       }
     }
     }
   }
   }
@@ -110,7 +249,7 @@ li.dropdown-item.dropdown-item-btns {
     padding-right: 0.5rem;
     padding-right: 0.5rem;
     position: sticky;
     position: sticky;
     height: 8rem;
     height: 8rem;
-    background-color: $light;
+    background-color: var(--nbx-sidebar-bg);
     box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
     box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
     .nav-link {
     .nav-link {
       padding: 0.5rem 0.25rem;
       padding: 0.5rem 0.25rem;
@@ -132,19 +271,6 @@ li.dropdown-item.dropdown-item-btns {
   white-space: nowrap !important;
   white-space: nowrap !important;
 }
 }
 
 
-h1.accordion-item-title,
-h2.accordion-item-title,
-h3.accordion-item-title,
-h4.accordion-item-title,
-h5.accordion-item-title,
-h6.accordion-item-title {
-  padding: 0 0.5rem;
-  font-weight: $font-weight-bold;
-  text-transform: uppercase;
-  color: $text-muted;
-  font-size: $font-size-sm;
-}
-
 #object-type-selector {
 #object-type-selector {
   button.dropdown-item,
   button.dropdown-item,
   h6.dropdown-header {
   h6.dropdown-header {
@@ -167,8 +293,8 @@ span.color-label {
 
 
 pre {
 pre {
   border-radius: $border-radius;
   border-radius: $border-radius;
-  border: 1px solid $gray-200;
-  background-color: $gray-100;
+  border: 1px solid var(--nbx-pre-border-color);
+  background-color: var(--nbx-pre-bg);
   padding: $spacer;
   padding: $spacer;
   white-space: pre;
   white-space: pre;
 }
 }
@@ -273,7 +399,6 @@ table tr.vertical-align {
   vertical-align: middle;
   vertical-align: middle;
 }
 }
 
 
-// Pad all adjacent cards
 .card:not(:only-of-type) {
 .card:not(:only-of-type) {
   margin-bottom: $spacer;
   margin-bottom: $spacer;
 }
 }
@@ -285,9 +410,9 @@ table tr.vertical-align {
 nav.breadcrumb-container {
 nav.breadcrumb-container {
   padding: $badge-padding-y $badge-padding-x;
   padding: $badge-padding-y $badge-padding-x;
   border-radius: $border-radius;
   border-radius: $border-radius;
-  background-color: $light;
   font-size: $font-size-sm;
   font-size: $font-size-sm;
   width: fit-content;
   width: fit-content;
+  background-color: var(--nbx-breadcrumb-bg);
 
 
   ol.breadcrumb {
   ol.breadcrumb {
     margin-bottom: 0;
     margin-bottom: 0;
@@ -310,21 +435,6 @@ div.paginator > form > div.input-group {
   width: fit-content;
   width: fit-content;
 }
 }
 
 
-button.btn.btn-outline-gray.dropdown-toggle:after {
-  color: $black;
-}
-
-// Apply bootstrap focus styling to Choices.JS elements.
-div.choices.is-focused > div.choices__inner {
-  border-color: $form-select-focus-border-color;
-  outline: 0;
-  @if $enable-shadows {
-    @include box-shadow($form-select-box-shadow, $form-select-focus-box-shadow);
-  } @else {
-    box-shadow: $form-select-focus-box-shadow;
-  }
-}
-
 div.field-group:not(:first-of-type) {
 div.field-group:not(:first-of-type) {
   margin-top: $spacer * 3;
   margin-top: $spacer * 3;
 
 
@@ -365,8 +475,13 @@ span.bi-plus:before {
   font-weight: $font-weight-bold !important;
   font-weight: $font-weight-bold !important;
 }
 }
 
 
-table tbody tr.success {
-  background-color: rgba($success, 0.15);
+table tbody {
+  @each $color, $value in $theme-colors {
+    tr.#{$color} {
+      background-color: rgba($value, 0.15);
+      border-color: $gray-500;
+    }
+  }
 }
 }
 table td,
 table td,
 table th {
 table th {
@@ -380,16 +495,16 @@ table th {
   text-align: center;
   text-align: center;
 }
 }
 .cable-trace .node {
 .cable-trace .node {
-  background-color: $gray-100;
-  border: $border-width solid $gray-200;
+  background-color: var(--nbx-cable-node-bg);
+  border: $border-width solid var(--nbx-cable-node-border-color);
   border-radius: $border-radius;
   border-radius: $border-radius;
   padding: 1.5rem 1rem;
   padding: 1.5rem 1rem;
   position: relative;
   position: relative;
   z-index: 1;
   z-index: 1;
 }
 }
 .cable-trace .termination {
 .cable-trace .termination {
-  background-color: $gray-200;
-  border: $border-width solid $gray-300;
+  background-color: var(--nbx-cable-termination-bg);
+  border: $border-width solid var(--nbx-cable-termination-border-color);
   box-shadow: $box-shadow;
   box-shadow: $box-shadow;
   border-radius: $border-radius;
   border-radius: $border-radius;
   margin: -1rem auto;
   margin: -1rem auto;
@@ -399,7 +514,7 @@ table th {
   z-index: 2;
   z-index: 2;
 }
 }
 .cable-trace .active {
 .cable-trace .active {
-  border: 0.25rem solid $green;
+  border: 0.25rem solid $success;
 }
 }
 .cable-trace .cable {
 .cable-trace .cable {
   border-left-style: solid;
   border-left-style: solid;
@@ -422,10 +537,10 @@ pre.change-data {
     padding-left: $spacer;
     padding-left: $spacer;
     padding-right: $spacer;
     padding-right: $spacer;
     &.added {
     &.added {
-      background-color: rgba($green, 0.4);
+      background-color: var(--nbx-change-added);
     }
     }
     &.removed {
     &.removed {
-      background-color: rgba($red, 0.4);
+      background-color: var(--nbx-change-removed);
     }
     }
   }
   }
 }
 }
@@ -433,10 +548,10 @@ pre.change-data {
 pre.change-diff {
 pre.change-diff {
   border-color: transparent;
   border-color: transparent;
   &.change-removed {
   &.change-removed {
-    background-color: rgba($red, 0.4);
+    background-color: var(--nbx-change-removed);
   }
   }
   &.change-added {
   &.change-added {
-    background-color: rgba($green, 0.4);
+    background-color: var(--nbx-change-added);
   }
   }
 }
 }
 
 

+ 1 - 3
netbox/project-static/package.json

@@ -4,9 +4,7 @@
   "main": "dist/netbox.js",
   "main": "dist/netbox.js",
   "license": "Apache-2.0",
   "license": "Apache-2.0",
   "scripts": {
   "scripts": {
-    "bundle:css": "parcel build --public-url /static -o netbox.css main.scss && parcel build --public-url /static -o rack_elevation.css rack_elevation.scss",
-    "bundle:js": "parcel build --public-url /static -o netbox.js src/index.ts && parcel build --public-url /static -o jobs.js src/jobs.ts && parcel build --public-url /static -o lldp.js src/device/lldp.ts && parcel build --public-url /static -o config.js src/device/config.ts && parcel build --public-url /static -o status.js src/device/status.ts",
-    "bundle": "yarn bundle:css && yarn bundle:js"
+    "bundle": "node bundle.js"
   },
   },
   "dependencies": {
   "dependencies": {
     "@mdi/font": "^5.9.55",
     "@mdi/font": "^5.9.55",

+ 90 - 56
netbox/project-static/rack_elevation.scss

@@ -1,5 +1,6 @@
 /* Stylesheet for rendering SVG rack elevations */
 /* Stylesheet for rendering SVG rack elevations */
-@import './theme.scss';
+@import './theme-light.scss';
+
 * {
 * {
   font-family: $font-family-sans-serif;
   font-family: $font-family-sans-serif;
   font-size: $font-size-sm;
   font-size: $font-size-sm;
@@ -11,59 +12,92 @@ text {
   text-anchor: middle;
   text-anchor: middle;
   dominant-baseline: middle;
   dominant-baseline: middle;
 }
 }
-.rack {
-  background-color: $gray-100;
-  fill: none;
-  stroke: black;
-  stroke-width: 2px;
-}
-.slot {
-  fill: $gray-200;
-  stroke: $gray-500;
-}
-.slot:hover {
-  fill: $white;
-}
-.slot + .add-device {
-  fill: none;
-}
-.slot:hover + .add-device {
-  fill: $primary;
-}
-.add-device:hover {
-  fill: $primary;
-}
-.add-device:hover + .slot {
-  fill: $white;
-}
-.reserved {
-  fill: url(#reserved);
-}
-.reserved:hover {
-  fill: url(#reserved);
-}
-.occupied {
-  fill: url(#occupied);
-}
-.occupied:hover {
-  fill: url(#occupied);
-}
-.blocked {
-  fill: url(#blocked);
-}
-.blocked:hover {
-  fill: url(#blocked);
-}
-.blocked:hover + .add-device {
-  fill: none;
-}
-.unit {
-  margin: 0;
-  padding: 5px 0px;
-  fill: $gray-400;
-  font-size: $font-size-sm;
-  font-family: $font-family-sans-serif;
-}
-.hidden {
-  visibility: hidden;
+
+svg {
+  .rack {
+    background-color: $gray-100;
+    fill: none;
+    stroke: $body-color;
+    stroke-width: 2px;
+  }
+  .slot {
+    fill: $gray-100;
+    stroke: $gray-500;
+    &:hover {
+      fill: $gray-50;
+    }
+    & + .add-device {
+      fill: none;
+    }
+    &:hover + .add-device {
+      fill: $blue;
+    }
+    & .add-device {
+      &:hover {
+        fill: $blue;
+      }
+      &:hover + .slot {
+        fill: $white;
+      }
+    }
+    &.reserved[class] {
+      fill: url(#reserved);
+    }
+    &.reserved:hover[class] {
+      fill: url(#reserved);
+    }
+    &.occupied[class] {
+      fill: url(#occupied);
+    }
+    &.occupied:hover[class] {
+      fill: url(#occupied);
+    }
+    &.blocked[class] {
+      fill: url(#blocked);
+    }
+    &.blocked:hover[class] {
+      fill: url(#blocked);
+    }
+    &.blocked:hover + .add-device {
+      fill: none;
+    }
+  }
+
+  .unit {
+    margin: 0;
+    padding: 5px 0px;
+    fill: $gray-400;
+    font-size: $font-size-sm;
+    font-family: $font-family-sans-serif;
+  }
+  .hidden {
+    visibility: hidden;
+  }
+
+  &[data-netbox-color-mode='dark'] {
+    .rack {
+      background-color: $gray-800;
+    }
+    .slot {
+      fill: $gray-700;
+      stroke: $gray-400;
+      &:hover {
+        fill: $gray-600;
+      }
+      & + .add-device {
+        fill: none;
+      }
+      &:hover + .add-device {
+        fill: $blue-300;
+      }
+    }
+    .add-device {
+      &:hover {
+        fill: $blue-300;
+      }
+      &:hover + .slot {
+        fill: $black;
+      }
+    }
+  }
 }
 }

+ 13 - 0
netbox/project-static/select.scss

@@ -19,6 +19,8 @@ div.form-floating div.ss-main div.ss-multi-selected {
 @import './node_modules/slim-select/src/slim-select/slimselect.scss';
 @import './node_modules/slim-select/src/slim-select/slimselect.scss';
 
 
 .ss-main {
 .ss-main {
+  color: $form-select-color;
+
   &.is-invalid .ss-single-selected,
   &.is-invalid .ss-single-selected,
   &.is-invalid .ss-multi-selected {
   &.is-invalid .ss-multi-selected {
     border-color: $form-feedback-icon-invalid-color;
     border-color: $form-feedback-icon-invalid-color;
@@ -39,6 +41,7 @@ div.form-floating div.ss-main div.ss-multi-selected {
   }
   }
 
 
   .ss-single-selected {
   .ss-single-selected {
+    background-color: $form-select-bg;
     span.ss-arrow {
     span.ss-arrow {
       // Inherit the arrow color from the parent (see color selector).
       // Inherit the arrow color from the parent (see color selector).
       span.arrow-down,
       span.arrow-down,
@@ -56,6 +59,7 @@ div.form-floating div.ss-main div.ss-multi-selected {
     align-items: center;
     align-items: center;
     padding-left: $input-padding-x;
     padding-left: $input-padding-x;
     padding-right: $input-padding-x;
     padding-right: $input-padding-x;
+    background-color: $form-select-bg;
 
 
     .ss-values {
     .ss-values {
       padding-top: $spacer * 2 !important;
       padding-top: $spacer * 2 !important;
@@ -69,7 +73,15 @@ div.form-floating div.ss-main div.ss-multi-selected {
   }
   }
 
 
   .ss-content {
   .ss-content {
+    background-color: $gray-900;
     .ss-list {
     .ss-list {
+      .ss-option.ss-option-selected {
+        background-color: $gray-600;
+        color: $white;
+      }
+      .ss-option:hover {
+        background-color: $blue-400;
+      }
       .ss-option:last-child {
       .ss-option:last-child {
         border-bottom-left-radius: $form-select-border-radius;
         border-bottom-left-radius: $form-select-border-radius;
         border-bottom-right-radius: $form-select-border-radius;
         border-bottom-right-radius: $form-select-border-radius;
@@ -79,6 +91,7 @@ div.form-floating div.ss-main div.ss-multi-selected {
     border-bottom-right-radius: $form-select-border-radius;
     border-bottom-right-radius: $form-select-border-radius;
     .ss-search {
     .ss-search {
       input[type='search'] {
       input[type='search'] {
+        background-color: $form-select-bg;
         border: $form-select-border-width solid $form-select-border-color;
         border: $form-select-border-width solid $form-select-border-color;
         &:focus {
         &:focus {
           border-color: $form-select-focus-border-color;
           border-color: $form-select-focus-border-color;

+ 134 - 0
netbox/project-static/src/colorMode.ts

@@ -0,0 +1,134 @@
+import { getElements, isTruthy } from './util';
+
+type ColorMode = 'light' | 'dark';
+type ColorModePreference = ColorMode | 'none';
+
+const COLOR_MODE_KEY = 'netbox-color-mode';
+const TEXT_WHEN_DARK = 'Light Mode';
+const TEXT_WHEN_LIGHT = 'Dark Mode';
+const ICON_WHEN_DARK = 'mdi-lightbulb-on';
+const ICON_WHEN_LIGHT = 'mdi-lightbulb';
+
+function isColorMode(value: string): value is ColorMode {
+  return value === 'dark' || value === 'light';
+}
+
+/**
+ * Set the color mode to light or dark.
+ *
+ * @param mode `'light'` or `'dark'`
+ * @returns `true` if the color mode was successfully set, `false` if not.
+ */
+function storeColorMode(mode: ColorMode): void {
+  return localStorage.setItem(COLOR_MODE_KEY, mode);
+}
+
+function updateElements(targetMode: ColorMode): void {
+  document.body.setAttribute(`data-${COLOR_MODE_KEY}`, targetMode);
+
+  for (const text of getElements<HTMLSpanElement>('span.color-mode-text')) {
+    if (targetMode === 'light') {
+      text.innerText = TEXT_WHEN_LIGHT;
+    } else if (targetMode === 'dark') {
+      text.innerText = TEXT_WHEN_DARK;
+    }
+  }
+  for (const icon of getElements<HTMLSpanElement>('i.color-mode-icon', 'span.color-mode-icon')) {
+    if (targetMode === 'light') {
+      icon.classList.remove(ICON_WHEN_DARK);
+      icon.classList.add(ICON_WHEN_LIGHT);
+    } else if (targetMode === 'dark') {
+      icon.classList.remove(ICON_WHEN_LIGHT);
+      icon.classList.add(ICON_WHEN_DARK);
+    }
+  }
+
+  for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
+    const svg = elevation.contentDocument?.querySelector('svg') ?? null;
+    if (svg !== null) {
+      svg.setAttribute(`data-${COLOR_MODE_KEY}`, targetMode);
+    }
+  }
+}
+
+/**
+ * Call all functions necessary to update the color mode across the UI.
+ *
+ * @param mode Target color mode.
+ */
+function setColorMode(mode: ColorMode): void {
+  for (const func of [storeColorMode, updateElements]) {
+    func(mode);
+  }
+}
+
+/**
+ * Toggle the color mode when a color mode toggle is clicked.
+ */
+function handleColorModeToggle(): void {
+  const currentValue = localStorage.getItem(COLOR_MODE_KEY);
+  if (currentValue === 'light') {
+    setColorMode('dark');
+  } else if (currentValue === 'dark') {
+    setColorMode('light');
+  } else {
+    console.warn('Unable to determine the current color mode');
+  }
+}
+
+/**
+ * Determine the user's preference and set it as the color mode.
+ */
+function defaultColorMode(): void {
+  // Get the current color mode value from local storage.
+  const currentValue = localStorage.getItem(COLOR_MODE_KEY) as Nullable<ColorMode>;
+  const bodyValue = document.body.getAttribute(`data-${COLOR_MODE_KEY}`);
+
+  if (isTruthy(bodyValue) && isTruthy(currentValue)) {
+    return setColorMode(currentValue);
+  }
+
+  let preference: ColorModePreference = 'none';
+
+  // Determine if the user prefers dark or light mode.
+  for (const mode of ['dark', 'light']) {
+    if (window.matchMedia(`(prefers-color-scheme: ${mode})`).matches) {
+      preference = mode as ColorModePreference;
+      break;
+    }
+  }
+
+  if (isTruthy(currentValue) && !isTruthy(bodyValue) && isColorMode(currentValue)) {
+    return setColorMode(currentValue);
+  }
+
+  switch (preference) {
+    case 'dark':
+      return setColorMode('dark');
+    case 'light':
+      return setColorMode('light');
+    case 'none':
+      return setColorMode('light');
+    default:
+      return setColorMode('light');
+  }
+}
+
+/**
+ * Initialize color mode toggle buttons and set the default color mode.
+ */
+function initColorModeToggle(): void {
+  for (const element of getElements<HTMLButtonElement>('button.color-mode-toggle')) {
+    element.addEventListener('click', handleColorModeToggle);
+  }
+}
+
+/**
+ * Initialize all color mode elements.
+ */
+export function initColorMode(): void {
+  window.addEventListener('load', defaultColorMode);
+  for (const func of [initColorModeToggle]) {
+    func();
+  }
+}

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

@@ -4,15 +4,16 @@ import { initSearch } from './search';
 import { initSelect } from './select';
 import { initSelect } from './select';
 import { initButtons } from './buttons';
 import { initButtons } from './buttons';
 import { initSecrets } from './secrets';
 import { initSecrets } from './secrets';
+import { initColorMode } from './colorMode';
 import { initMessages } from './messages';
 import { initMessages } from './messages';
 import { initClipboard } from './clipboard';
 import { initClipboard } from './clipboard';
 import { initDateSelector } from './dateSelector';
 import { initDateSelector } from './dateSelector';
-
 import { initTableConfig } from './tableConfig';
 import { initTableConfig } from './tableConfig';
 
 
 function init() {
 function init() {
   for (const init of [
   for (const init of [
     initBootstrap,
     initBootstrap,
+    initColorMode,
     initMessages,
     initMessages,
     initForms,
     initForms,
     initSearch,
     initSearch,

+ 211 - 0
netbox/project-static/theme-base.scss

@@ -0,0 +1,211 @@
+@import 'bootstrap/scss/functions';
+
+$alt: #13293d;
+$darker: #010101;
+
+$gray: #6b7280;
+$red: #ef4444;
+$yellow: #f59e0b;
+$green: #10b981;
+$blue: #3b82f6;
+$purple: #8b5cf6;
+$pink: #ec4899;
+
+$gray-50: #f9fafb;
+$gray-100: #f3f4f6;
+$gray-200: #e5e7eb;
+$gray-300: #d1d5db;
+$gray-400: #9ca3af;
+$gray-500: #6b7280;
+$gray-600: #4b5563;
+$gray-700: #374151;
+$gray-800: #1f2937;
+$gray-900: #111827;
+
+$red-50: #fef2f2;
+$red-100: #fee2e2;
+$red-200: #fecaca;
+$red-300: #fca5a5;
+$red-400: #f87171;
+$red-500: #ef4444;
+$red-600: #dc2626;
+$red-700: #b91c1c;
+$red-800: #991b1b;
+$red-900: #7f1d1d;
+
+$yellow-50: #fffbeb;
+$yellow-100: #fef3c7;
+$yellow-200: #fde68a;
+$yellow-300: #fcd34d;
+$yellow-400: #fbbf24;
+$yellow-500: #f59e0b;
+$yellow-600: #d97706;
+$yellow-700: #b45309;
+$yellow-800: #92400e;
+$yellow-900: #78350f;
+
+$green-50: #ecfdf5;
+$green-100: #d1fae5;
+$green-200: #a7f3d0;
+$green-300: #6ee7b7;
+$green-400: #34d399;
+$green-500: #10b981;
+$green-600: #059669;
+$green-700: #047857;
+$green-800: #065f46;
+$green-900: #064e3b;
+
+$blue-50: #eff6ff;
+$blue-100: #dbeafe;
+$blue-200: #bfdbfe;
+$blue-300: #93c5fd;
+$blue-400: #60a5fa;
+$blue-500: #3b82f6;
+$blue-600: #2563eb;
+$blue-700: #1d4ed8;
+$blue-800: #1e40af;
+$blue-900: #1e3a8a;
+
+$indigo-50: #eef2ff;
+$indigo-100: #e0e7ff;
+$indigo-200: #c7d2fe;
+$indigo-300: #a5b4fc;
+$indigo-400: #818cf8;
+$indigo-500: #6366f1;
+$indigo-600: #4f46e5;
+$indigo-700: #4338ca;
+$indigo-800: #3730a3;
+$indigo-900: #312e81;
+
+$purple-50: #f5f3ff;
+$purple-100: #ede9fe;
+$purple-200: #ddd6fe;
+$purple-300: #c4b5fd;
+$purple-400: #a78bfa;
+$purple-500: #8b5cf6;
+$purple-600: #7c3aed;
+$purple-700: #6d28d9;
+$purple-800: #5b21b6;
+$purple-900: #4c1d95;
+
+$pink-50: #fdf2f8;
+$pink-100: #fce7f3;
+$pink-200: #fbcfe8;
+$pink-300: #f9a8d4;
+$pink-400: #f472b6;
+$pink-500: #ec4899;
+$pink-600: #db2777;
+$pink-700: #be185d;
+$pink-800: #9d174d;
+$pink-900: #831843;
+
+$card-cap-bg: 'unset';
+
+$border-radius-md: 0.375rem;
+$border-radius-lg: 0.5rem;
+$border-radius-xl: 0.75rem;
+$border-radius-2xl: 1.5rem;
+
+$border-radius: $border-radius-lg;
+
+$border-radius-sm: $border-radius;
+$border-radius-lg: $border-radius-xl;
+
+$badge-border-radius: $border-radius-md;
+$progress-border-radius: $border-radius-md;
+
+$font-weight-lighter: 200;
+$font-weight-medium: 600;
+$font-weight-bolder: 800;
+
+$theme-color-addons: (
+  'alt': $alt,
+  'gray': $gray-400,
+  'darker': $darker,
+  'gray-50': $gray-50,
+  'gray-100': $gray-100,
+  'gray-200': $gray-200,
+  'gray-300': $gray-300,
+  'gray-400': $gray-400,
+  'gray-500': $gray-500,
+  'gray-600': $gray-600,
+  'gray-700': $gray-700,
+  'gray-800': $gray-800,
+  'gray-900': $gray-900,
+  'red-50': $red-50,
+  'red-100': $red-100,
+  'red-200': $red-200,
+  'red-300': $red-300,
+  'red-400': $red-400,
+  'red-500': $red-500,
+  'red-600': $red-600,
+  'red-700': $red-700,
+  'red-800': $red-800,
+  'red-900': $red-900,
+  'yellow-50': $yellow-50,
+  'yellow-100': $yellow-100,
+  'yellow-200': $yellow-200,
+  'yellow-300': $yellow-300,
+  'yellow-400': $yellow-400,
+  'yellow-500': $yellow-500,
+  'yellow-600': $yellow-600,
+  'yellow-700': $yellow-700,
+  'yellow-800': $yellow-800,
+  'yellow-900': $yellow-900,
+  'green-50': $green-50,
+  'green-100': $green-100,
+  'green-200': $green-200,
+  'green-300': $green-300,
+  'green-400': $green-400,
+  'green-500': $green-500,
+  'green-600': $green-600,
+  'green-700': $green-700,
+  'green-800': $green-800,
+  'green-900': $green-900,
+  'blue-50': $blue-50,
+  'blue-100': $blue-100,
+  'blue-200': $blue-200,
+  'blue-300': $blue-300,
+  'blue-400': $blue-400,
+  'blue-500': $blue-500,
+  'blue-600': $blue-600,
+  'blue-700': $blue-700,
+  'blue-800': $blue-800,
+  'blue-900': $blue-900,
+  'indigo-50': $indigo-50,
+  'indigo-100': $indigo-100,
+  'indigo-200': $indigo-200,
+  'indigo-300': $indigo-300,
+  'indigo-400': $indigo-400,
+  'indigo-500': $indigo-500,
+  'indigo-600': $indigo-600,
+  'indigo-700': $indigo-700,
+  'indigo-800': $indigo-800,
+  'indigo-900': $indigo-900,
+  'purple-50': $purple-50,
+  'purple-100': $purple-100,
+  'purple-200': $purple-200,
+  'purple-300': $purple-300,
+  'purple-400': $purple-400,
+  'purple-500': $purple-500,
+  'purple-600': $purple-600,
+  'purple-700': $purple-700,
+  'purple-800': $purple-800,
+  'purple-900': $purple-900,
+  'pink-50': $pink-50,
+  'pink-100': $pink-100,
+  'pink-200': $pink-200,
+  'pink-300': $pink-300,
+  'pink-400': $pink-400,
+  'pink-500': $pink-500,
+  'pink-600': $pink-600,
+  'pink-700': $pink-700,
+  'pink-800': $pink-800,
+  'pink-900': $pink-900,
+);
+
+$font-family-sans-serif: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
+  Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+  'Segoe UI Symbol', 'Noto Color Emoji';
+$font-family-monospace: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
+  'Courier New', monospace;

+ 292 - 0
netbox/project-static/theme-dark.scss

@@ -0,0 +1,292 @@
+@import './theme-base.scss';
+
+$primary: $blue-300;
+$secondary: $gray-400;
+$success: $green-300;
+$info: $cyan-300;
+$warning: $yellow-300;
+$danger: $red-300;
+$light: $gray-300;
+$dark: $gray-400;
+
+$theme-colors: (
+  'primary': $primary,
+  'secondary': $secondary,
+  'success': $success,
+  'info': $info,
+  'warning': $warning,
+  'danger': $danger,
+  'light': $light,
+  'dark': $dark,
+);
+
+$theme-color-addons-dark: (
+  'alt': #13293d,
+  'darker': #010101,
+);
+
+$theme-colors: map-merge($theme-colors, $theme-color-addons);
+$theme-color-addons: map-merge($theme-color-addons, $theme-color-addons-dark);
+
+// On import, any variables marked `!default` will be overridden by the above.
+@import 'bootstrap/scss/variables';
+
+// Customize the light and dark text colors for use in our color contrast function.
+
+// Gradient
+$gradient: linear-gradient(180deg, rgba($white, 0.15), rgba($white, 0));
+
+// Body
+$body-bg: $gray-900;
+$body-color: $white;
+$body-text-align: null;
+$border-color: $gray-700;
+$box-shadow: 0 0.5rem 1rem rgba($black, 0.15);
+$box-shadow-sm: 0 0.125rem 0.25rem rgba($black, 0.075);
+$box-shadow-lg: 0 1rem 3rem rgba($black, 0.175);
+$box-shadow-inset: inset 0 1px 2px rgba($black, 0.075);
+// $component-active-color:      $white;
+// $component-active-bg:         $primary;
+$text-muted: $gray-500;
+$blockquote-footer-color: $gray-600;
+$mark-bg: #fcf8e3;
+
+// Tables
+$table-color: $gray-100;
+$table-border-color: $border-color;
+// $table-bg:                    transparent;
+$table-striped-color: $table-color;
+$table-striped-bg: rgba($white, $table-striped-bg-factor);
+$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-group-separator-color: currentColor;
+
+// Buttons + Forms
+// $input-btn-focus-color:         rgba($component-active-bg, $input-btn-focus-color-opacity);
+// $input-btn-focus-box-shadow:    0 0 0 $input-btn-focus-width $input-btn-focus-color;
+
+// Buttons
+$btn-box-shadow: inset 0 1px 0 rgba($black, 0.15), 0 1px 1px rgba($white, 0.075);
+$btn-active-box-shadow: inset 0 3px 5px rgba($white, 0.125);
+// $btn-link-color:              $link-color;
+// $btn-link-hover-color:        $link-hover-color;
+$btn-link-disabled-color: $gray-300;
+
+// Forms
+$form-text-color: $text-muted;
+$input-bg: $gray-800;
+$input-disabled-bg: $gray-700;
+$input-color: $gray-100;
+$input-border-color: $gray-700;
+$input-focus-bg: $input-bg;
+$input-focus-border-color: tint-color($component-active-bg, 10%);
+$input-focus-color: $input-color;
+$input-placeholder-color: $gray-300;
+$input-plaintext-color: $body-color;
+
+$form-check-input-active-filter: brightness(90%);
+$form-check-input-bg: $input-bg;
+$form-check-input-border: 1px solid rgba(255, 255, 255, 0.25);
+$form-check-input-checked-color: $component-active-color;
+$form-check-input-checked-bg-color: $component-active-bg;
+$form-check-input-checked-border-color: $form-check-input-checked-bg-color;
+$form-check-input-indeterminate-color: $component-active-color;
+$form-check-input-indeterminate-bg-color: $component-active-bg;
+$form-check-input-indeterminate-border-color: $form-check-input-indeterminate-bg-color;
+
+$form-switch-color: rgba(255, 255, 255, 0.25);
+$form-switch-focus-color: $input-focus-border-color;
+$form-switch-checked-color: $component-active-color;
+
+$input-group-addon-color: $input-color;
+$input-group-addon-bg: $gray-700;
+$input-group-addon-border-color: $input-border-color;
+
+$form-select-color: $input-color;
+$form-select-disabled-color: $gray-400;
+$form-select-bg: $input-bg;
+$form-select-disabled-bg: $input-disabled-bg;
+$form-select-indicator-color: $gray-800;
+
+$form-select-border-color: $input-border-color;
+$form-range-track-bg: $gray-300;
+
+$form-range-thumb-bg: $component-active-bg;
+$form-range-thumb-box-shadow: 0 0.1rem 0.25rem rgba($black, 0.1);
+$form-range-thumb-focus-box-shadow: 0 0 0 1px $body-bg, $input-focus-box-shadow;
+$form-range-thumb-active-bg: tint-color($component-active-bg, 70%);
+$form-range-thumb-disabled-bg: $gray-500;
+
+$form-file-button-color: $input-color;
+$form-file-button-bg: $input-group-addon-bg;
+$form-file-button-hover-bg: shade-color($form-file-button-bg, 5%);
+
+// Navs
+$nav-link-color: $body-color;
+$nav-link-hover-color: null;
+$nav-link-disabled-color: $gray-800;
+$nav-tabs-border-color: $border-color;
+$nav-tabs-link-hover-border-color: rgba($gray-800, 0.5) rgba($gray-800, 0.5) $nav-tabs-border-color;
+$nav-tabs-link-active-color: $gray-50;
+$nav-tabs-link-active-bg: $body-bg;
+$nav-tabs-link-active-border-color: $gray-800 $gray-800 $nav-tabs-link-active-bg;
+$nav-pills-link-active-color: $component-active-color;
+$nav-pills-link-active-bg: $component-active-bg;
+
+// Dropdowns
+$dropdown-color: $body-color;
+$dropdown-bg: $gray-900;
+$dropdown-border-color: rgba($white, 0.15);
+$dropdown-link-color: $gray-100;
+$dropdown-link-hover-color: shade-color($gray-50, 10%);
+$dropdown-link-hover-bg: $gray-500;
+$dropdown-link-disabled-color: $gray-800;
+$dropdown-header-color: $gray-300;
+// $dropdown-dark-color:               $gray-300;
+// $dropdown-dark-bg:                  $gray-800;
+// $dropdown-dark-border-color:        $dropdown-border-color;
+// $dropdown-dark-divider-bg:          $dropdown-divider-bg;
+// $dropdown-dark-box-shadow:          null;
+// $dropdown-dark-link-color:          $dropdown-dark-color;
+// $dropdown-dark-link-hover-color:    $white;
+// $dropdown-dark-link-hover-bg:       rgba($white, .15);
+// $dropdown-dark-link-active-color:   $dropdown-link-active-color;
+// $dropdown-dark-link-active-bg:      $dropdown-link-active-bg;
+// $dropdown-dark-link-disabled-color: $gray-500;
+// $dropdown-dark-header-color:        $gray-500;
+
+// Pagination
+$pagination-color: $link-color;
+$pagination-bg: $gray-800;
+$pagination-border-color: $gray-600;
+$pagination-focus-color: $link-hover-color;
+$pagination-focus-bg: $gray-400;
+$pagination-hover-color: $link-hover-color;
+$pagination-hover-bg: $gray-400;
+$pagination-hover-border-color: $gray-500;
+$pagination-active-color: $component-active-color;
+$pagination-active-bg: $component-active-bg;
+$pagination-active-border-color: $pagination-active-bg;
+$pagination-disabled-color: $gray-600;
+$pagination-disabled-bg: $gray-800;
+$pagination-disabled-border-color: $gray-600;
+
+// Cards
+$card-border-color: rgba($white, 0.125);
+$card-inner-border-radius: subtract($card-border-radius, $card-border-width);
+
+$card-cap-color: null;
+$card-height: null;
+$card-color: null;
+$card-bg: $gray-800;
+
+// Accordion
+$accordion-color: $body-color;
+// $accordion-bg:                            transparent;
+$accordion-border-color: rgba($white, 0.125);
+$accordion-button-color: $accordion-color;
+$accordion-button-bg: $accordion-bg;
+$accordion-button-active-bg: tint-color($component-active-bg, 5%);
+$accordion-button-active-color: shade-color($primary, 10%);
+$accordion-button-focus-border-color: $input-focus-border-color;
+$accordion-icon-color: $accordion-color;
+$accordion-icon-active-color: $accordion-button-active-color;
+
+$accordion-button-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$accordion-icon-color}'><path fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/></svg>");
+$accordion-button-active-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$accordion-icon-active-color}'><path fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/></svg>");
+
+// Tooltips
+$tooltip-color: $body-color;
+$tooltip-bg: $gray-700;
+// $tooltip-opacity:                   .9;
+$tooltip-arrow-color: $tooltip-bg;
+$form-feedback-tooltip-opacity: $tooltip-opacity;
+
+// Popovers
+$popover-bg: $gray-700;
+$popover-border-color: rgba($white, 0.2);
+$popover-header-bg: shade-color($popover-bg, 6%);
+$popover-header-color: $headings-color;
+$popover-body-color: $body-color;
+$popover-arrow-color: $popover-bg;
+$popover-arrow-outer-color: fade-in($popover-border-color, 0.05);
+
+// Toasts
+$toast-color: null;
+$toast-background-color: rgba($white, 0.85);
+$toast-border-color: rgba(0, 0, 0, 0.1);
+$toast-header-color: $gray-600;
+$toast-header-background-color: rgba($white, 0.85);
+$toast-header-border-color: rgba(0, 0, 0, 0.05);
+
+// Badges
+$badge-color: $white;
+
+// Modals
+$modal-content-color: null;
+$modal-content-bg: $gray-800;
+$modal-content-border-color: rgba($white, 0.2);
+$modal-backdrop-bg: $black;
+// $modal-backdrop-opacity:            .5;
+$modal-header-border-color: $border-color;
+$modal-footer-border-color: $modal-header-border-color;
+
+// Alerts
+// $alert-bg-scale:                    -80%;
+// $alert-border-scale:                -70%;
+// $alert-color-scale:                 40%;
+
+// Progress bars
+$progress-bg: $gray-600;
+$progress-bar-color: $white;
+$progress-bar-bg: $primary;
+
+// List group
+$list-group-color: null;
+$list-group-bg: $card-bg;
+$list-group-border-color: rgba($white, 0.125);
+$list-group-hover-bg: rgba($gray-50, 0.15);
+$list-group-active-color: $component-active-color;
+$list-group-active-bg: $component-active-bg;
+$list-group-active-border-color: $list-group-active-bg;
+// $list-group-disabled-color:         $gray-600;
+$list-group-disabled-bg: $list-group-bg;
+$list-group-action-color: $gray-300;
+$list-group-action-hover-color: $body-color;
+$list-group-action-active-color: $body-color;
+$list-group-action-active-bg: rgba($gray-300, 0.125);
+
+// Image thumbnails
+$thumbnail-bg: $body-bg;
+$thumbnail-border-color: $gray-300;
+
+// Figures
+$figure-caption-color: $gray-600;
+
+// Breadcrumbs
+// $breadcrumb-bg:                     $gray-700;
+$breadcrumb-divider-color: $gray-100;
+$breadcrumb-active-color: $body-color;
+$breadcrumb-divider-flipped: $breadcrumb-divider;
+$breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='#{$breadcrumb-divider-color}'/%3E%3C/svg%3E");
+
+// Carousel
+$carousel-control-color: $white;
+$carousel-indicator-active-bg: $white;
+$carousel-caption-color: $white;
+$carousel-dark-indicator-active-bg: $black;
+$carousel-dark-caption-color: $black;
+$carousel-dark-control-icon-filter: invert(1) grayscale(100);
+
+// Close
+$btn-close-color: $white;
+$btn-close-bg: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='#{$btn-close-color}'><path d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/></svg>");
+$btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
+
+// Code
+$code-color: $pink-300;
+$kbd-color: $white;
+$kbd-bg: $gray-300;
+$pre-color: null;

+ 6 - 14
netbox/project-static/theme.scss → netbox/project-static/theme-light.scss

@@ -1,24 +1,16 @@
-@import 'bootstrap/scss/functions';
+@import './theme-base.scss';
 
 
-// Override built-in variables/add new variables.
-$green: #47e5bc;
-$orange: #f9a620;
-// $yellow: #ffd449;
-$red: #ff5964;
-$alt: #13293d;
-
-$card-cap-bg: none;
+$input-border-color: $gray-200;
 
 
 // On import, any variables marked `!default` will be overridden by the above.
 // On import, any variables marked `!default` will be overridden by the above.
 @import 'bootstrap/scss/variables';
 @import 'bootstrap/scss/variables';
 
 
-$theme-color-addons: (
-  'alt': $alt,
-  'gray': $gray-400,
-);
-
 // Merge/modify bootstrap variables.
 // Merge/modify bootstrap variables.
+
 $theme-colors: map-merge($theme-colors, $theme-color-addons);
 $theme-colors: map-merge($theme-colors, $theme-color-addons);
+
+$light: $gray-100;
+
 $card-cap-color: $gray-800;
 $card-cap-color: $gray-800;
 
 
 $breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='currentColor'/%3E%3C/svg%3E");
 $breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='currentColor'/%3E%3C/svg%3E");

+ 1 - 1
netbox/project-static/tsconfig.json

@@ -7,7 +7,7 @@
     "esModuleInterop": true,
     "esModuleInterop": true,
     "isolatedModules": true,
     "isolatedModules": true,
     "noUnusedLocals": true,
     "noUnusedLocals": true,
-    "declaration": true,
+    "declaration": false,
     "module": "esnext",
     "module": "esnext",
     "target": "esnext",
     "target": "esnext",
     "jsx": "react",
     "jsx": "react",

+ 5 - 1
netbox/templates/base.html

@@ -22,7 +22,10 @@
     </script>
     </script>
     {% block head %}{% endblock %}
     {% block head %}{% endblock %}
   </head>
   </head>
-  <body>
+  {% with color_mode=preferences|get_key:'ui.colormode' %}
+  
+  <body{%if color_mode == 'dark'%} data-netbox-color-mode="dark"{% elif color_mode == 'light' %} data-netbox-color-mode="light"{% endif %}>
+  
     {% block layout %}{% endblock %}
     {% block layout %}{% endblock %}
     {% block javascript %}{% endblock %}
     {% block javascript %}{% endblock %}
     {% include './messages.html' %}
     {% include './messages.html' %}
@@ -30,4 +33,5 @@
     {% block data %}{% endblock %}
     {% block data %}{% endblock %}
     </div>
     </div>
   </body>
   </body>
+  {% endwith %}
 </html>
 </html>

+ 1 - 1
netbox/templates/changelog.html

@@ -13,7 +13,7 @@
   </thead>
   </thead>
   <tbody>
   <tbody>
     {% for change in changelog %}
     {% for change in changelog %}
-    <tr class="table-{% get_status change.get_action_display %}">
+    <tr class="{% get_status change.get_action_display %}">
       <th scope="row">{{ change.user|default:change.user_name }}</th>
       <th scope="row">{{ change.user|default:change.user_name }}</th>
       <td>{{ change.get_action_display|bettertitle }}</td>
       <td>{{ change.get_action_display|bettertitle }}</td>
       <td>{{ change.changed_object_type.name|bettertitle }}</td>
       <td>{{ change.changed_object_type.name|bettertitle }}</td>

+ 1 - 1
netbox/templates/inc/search_panel.html

@@ -4,7 +4,7 @@
     <h5 class="card-header">
     <h5 class="card-header">
         Search
         Search
     </h5>
     </h5>
-    <div class="card-body">
+    <div class="card-body overflow-visible">
         <form action="." method="get">
         <form action="." method="get">
             {% for field in filter_form.hidden_fields %}
             {% for field in filter_form.hidden_fields %}
                 {{ field }}
                 {{ field }}

+ 3 - 3
netbox/templates/layout.html

@@ -7,12 +7,12 @@
   <div class="row">
   <div class="row">
     <nav
     <nav
       id="sidebar-menu"
       id="sidebar-menu"
-      class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse px-0"
+      class="col-md-3 col-lg-2 d-md-block sidebar collapse px-0"
     >
     >
       <div class="position-sticky pt-3">
       <div class="position-sticky pt-3">
         <a class="px-2 sidebar-logo" href="{% url 'home' %}">
         <a class="px-2 sidebar-logo" href="{% url 'home' %}">
           {% load static %}
           {% load static %}
-          <img src="{% static 'netbox_logo.svg' %}" height="50" />
+          {% include 'logo.html' %}
         </a>
         </a>
         <ul class="nav flex-column">
         <ul class="nav flex-column">
           {% load nav %} {% nav %}
           {% load nav %} {% nav %}
@@ -22,7 +22,7 @@
     </nav>
     </nav>
 
 
     <main class="col-md-9 ms-sm-auto col-lg-10 px-0">
     <main class="col-md-9 ms-sm-auto col-lg-10 px-0">
-      <nav class="navbar navbar-light sticky-top flex-md-nowrap py-4 bg-white container-fluid">
+      <nav class="navbar navbar-light sticky-top flex-md-nowrap py-4 search container-fluid">
           <button
           <button
             type="button"
             type="button"
             aria-expanded="false"
             aria-expanded="false"

Разница между файлами не показана из-за своего большого размера
+ 18 - 0
netbox/templates/logo.html


+ 12 - 6
netbox/templates/profile_button.html

@@ -10,21 +10,27 @@
     <span id="navbar_user">{{ request.user|truncatechars:"30" }}</span>
     <span id="navbar_user">{{ request.user|truncatechars:"30" }}</span>
   </button>
   </button>
   <ul class="dropdown-menu dropdown-menu-end">
   <ul class="dropdown-menu dropdown-menu-end">
-    <li class="dropdown-item">
+    <li>
+      <button type="button" class="dropdown-item color-mode-toggle">
+        <i class="color-mode-icon mdi mdi-lightbulb"></i>&nbsp;
+        <span class="color-mode-text">Dark Mode</span>
+      </button>
+    </li>
+    <li>
       {% if request.user.is_staff %}
       {% if request.user.is_staff %}
-      <a class="text-decoration-none" href="{% url 'admin:index' %}">
+      <a class="dropdown-item" href="{% url 'admin:index' %}">
         <i class="mdi mdi-cog"></i> Admin
         <i class="mdi mdi-cog"></i> Admin
       </a>
       </a>
       {% endif %}
       {% endif %}
     </li>
     </li>
-    <li class="dropdown-item">
-      <a class="text-decoration-none" href="{% url 'user:profile' %}">
+    <li>
+      <a class="dropdown-item" href="{% url 'user:profile' %}">
         <i class="mdi mdi-account"></i> Profile
         <i class="mdi mdi-account"></i> Profile
       </a>
       </a>
     </li>
     </li>
     <li><hr class="dropdown-divider" /></li>
     <li><hr class="dropdown-divider" /></li>
-    <li class="dropdown-item">
-      <a class="text-decoration-none" href="{% url 'logout' %}">
+    <li>
+      <a class="dropdown-item" href="{% url 'logout' %}">
         <i class="mdi mdi-logout-variant"></i> Log Out
         <i class="mdi mdi-logout-variant"></i> Log Out
       </a>
       </a>
     </li>
     </li>

+ 51 - 27
netbox/templates/users/preferences.html

@@ -4,32 +4,56 @@
 {% block title %}User Preferences{% endblock %}
 {% block title %}User Preferences{% endblock %}
 
 
 {% block usercontent %}
 {% block usercontent %}
-    {% if preferences %}
-        <form method="post" action="">
-            {% csrf_token %}
-            <table class="table table-striped">
-                <thead>
-                    <tr>
-                        <th><input type="checkbox" class="toggle" title="Toggle all"></th>
-                        <th>Preference</th>
-                        <th>Value</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    {% for key, value in preferences.items %}
-                        <tr>
-                            <td class="min-width"><input type="checkbox" name="pk" value="{{ key }}"></td>
-                            <td><samp>{{ key }}</samp></td>
-                            <td><samp>{{ value }}</samp></td>
-                        </tr>
-                    {% endfor %}
-                </tbody>
-            </table>
-            <button type="submit" class="btn btn-danger">
-                <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Clear Selected
+<form method="post" action="">
+    {% csrf_token %}
+    <div class="field-group mb-3">
+        <h4>Color Mode</h4>
+        <p class="lead text-muted">Set your preferred UI color mode</p>
+        {% with color_mode=preferences|get_key:'ui.colormode'%}
+        <div class="form-check form-check-inline">
+            <input class="form-check-input" type="radio" name="ui.colormode" id="color-mode-preference-dark" value="dark"{% if color_mode == 'dark'%} checked{% endif %}>
+            <label class="form-check-label" for="color-mode-preference-dark">Dark</label>
+        </div>
+        <div class="form-check form-check-inline">
+            <input class="form-check-input" type="radio" name="ui.colormode" id="color-mode-preference-light" value="light"{% if color_mode == 'light'%} checked{% endif %}>
+            <label class="form-check-label" for="color-mode-preference-light">Light</label>
+        </div>
+        {% endwith %}
+    </div>
+    <div class="row">
+        <div class="col">
+            <button type="submit" class="btn btn-success" name="_update">
+                Save
             </button>
             </button>
-        </form>
-    {% else %}
-        <h3 class="text-muted text-center">No preferences found</h3>
-    {% endif %}
+        </div>
+    </div>
+{% if preferences %}
+<div class="field-group">
+    <h4>Other Preferences</h4>
+    <table class="table table-striped">
+        <thead>
+            <tr>
+                <th><input type="checkbox" class="toggle" title="Toggle all"></th>
+                <th>Preference</th>
+                <th>Value</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for key, value in preferences.items %}
+                <tr>
+                    <td class="min-width"><input type="checkbox" name="pk" value="{{ key }}"></td>
+                    <td><samp>{{ key }}</samp></td>
+                    <td><samp>{{ value }}</samp></td>
+                </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+    <button type="submit" class="btn btn-danger" name="_delete">
+        <span class="mdi mdi-trash-can-outline" aria-hidden="true"></span> Clear Selected
+    </button>
+</div>
+{% else %}
+    <h3 class="text-muted text-center">No preferences found</h3>
+{% endif %}
+</form>
 {% endblock %}
 {% endblock %}

+ 12 - 3
netbox/users/views.py

@@ -91,6 +91,7 @@ class LogoutView(View):
     """
     """
     Deauthenticate a web user.
     Deauthenticate a web user.
     """
     """
+
     def get(self, request):
     def get(self, request):
         logger = logging.getLogger('netbox.auth.logout')
         logger = logging.getLogger('netbox.auth.logout')
 
 
@@ -136,9 +137,17 @@ class UserConfigView(LoginRequiredMixin, View):
         data = userconfig.all()
         data = userconfig.all()
 
 
         # Delete selected preferences
         # Delete selected preferences
-        for key in request.POST.getlist('pk'):
-            if key in data:
-                userconfig.clear(key)
+        if "_delete" in request.POST:
+            for key in request.POST.getlist('pk'):
+                if key in data:
+                    userconfig.clear(key)
+        # Update specific values
+        elif "_update" in request.POST:
+            for key in request.POST:
+                if not key.startswith('_') and not key.contains('csrf'):
+                    for value in request.POST.getlist(key):
+                        userconfig.set(key, value)
+
         userconfig.save()
         userconfig.save()
         messages.success(request, "Your preferences have been updated.")
         messages.success(request, "Your preferences have been updated.")
 
 

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