implement dark mode

This commit is contained in:
checktheroads 2021-04-25 20:11:46 -07:00
parent d7d004b48e
commit 2b159fc40f
55 changed files with 1105 additions and 226 deletions

View File

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

View File

@ -0,0 +1 @@
dist

View File

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

View File

@ -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();

View File

@ -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;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,9 +1,21 @@
@import './theme.scss';
@import './theme-light.scss';
@import './bootstrap.scss';
@import './node_modules/@mdi/font/css/materialdesignicons.min.css';
@import '@mdi/font/css/materialdesignicons.min.css';
@import './select.scss';
@import './node_modules/flatpickr/dist/flatpickr.min.css';
@import 'flatpickr/dist/flatpickr.css';
@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';
}

View File

@ -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 {
display: flex;
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 {
width: 100%;
max-width: 330px;
@ -53,16 +182,6 @@ li.dropdown-item.dropdown-item-btns {
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) {
.sidebar {
top: 5rem;
@ -84,9 +203,28 @@ li.dropdown-item.dropdown-item-btns {
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 {
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 {
color: $gray-800;
color: var(--nbx-sidebar-link-color);
}
.accordion-body {
max-height: calc(100vh - 24rem);
@ -95,10 +233,11 @@ li.dropdown-item.dropdown-item-btns {
.nav-link {
padding: 0.25rem 0.5rem;
font-size: $font-size-base;
}
.nav-link:hover {
background-color: $blue-100;
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;
position: sticky;
height: 8rem;
background-color: $light;
background-color: var(--nbx-sidebar-bg);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
.nav-link {
padding: 0.5rem 0.25rem;
@ -132,19 +271,6 @@ li.dropdown-item.dropdown-item-btns {
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 {
button.dropdown-item,
h6.dropdown-header {
@ -167,8 +293,8 @@ span.color-label {
pre {
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;
white-space: pre;
}
@ -273,7 +399,6 @@ table tr.vertical-align {
vertical-align: middle;
}
// Pad all adjacent cards
.card:not(:only-of-type) {
margin-bottom: $spacer;
}
@ -285,9 +410,9 @@ table tr.vertical-align {
nav.breadcrumb-container {
padding: $badge-padding-y $badge-padding-x;
border-radius: $border-radius;
background-color: $light;
font-size: $font-size-sm;
width: fit-content;
background-color: var(--nbx-breadcrumb-bg);
ol.breadcrumb {
margin-bottom: 0;
@ -310,21 +435,6 @@ div.paginator > form > div.input-group {
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) {
margin-top: $spacer * 3;
@ -365,8 +475,13 @@ span.bi-plus:before {
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 th {
@ -380,16 +495,16 @@ table th {
text-align: center;
}
.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;
padding: 1.5rem 1rem;
position: relative;
z-index: 1;
}
.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;
border-radius: $border-radius;
margin: -1rem auto;
@ -399,7 +514,7 @@ table th {
z-index: 2;
}
.cable-trace .active {
border: 0.25rem solid $green;
border: 0.25rem solid $success;
}
.cable-trace .cable {
border-left-style: solid;
@ -422,10 +537,10 @@ pre.change-data {
padding-left: $spacer;
padding-right: $spacer;
&.added {
background-color: rgba($green, 0.4);
background-color: var(--nbx-change-added);
}
&.removed {
background-color: rgba($red, 0.4);
background-color: var(--nbx-change-removed);
}
}
}
@ -433,10 +548,10 @@ pre.change-data {
pre.change-diff {
border-color: transparent;
&.change-removed {
background-color: rgba($red, 0.4);
background-color: var(--nbx-change-removed);
}
&.change-added {
background-color: rgba($green, 0.4);
background-color: var(--nbx-change-added);
}
}

View File

@ -4,9 +4,7 @@
"main": "dist/netbox.js",
"license": "Apache-2.0",
"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": {
"@mdi/font": "^5.9.55",

View File

@ -1,5 +1,6 @@
/* Stylesheet for rendering SVG rack elevations */
@import './theme.scss';
@import './theme-light.scss';
* {
font-family: $font-family-sans-serif;
font-size: $font-size-sm;
@ -11,59 +12,92 @@ text {
text-anchor: 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;
}
}
}
}

View File

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

View File

@ -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();
}
}

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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.
@import 'bootstrap/scss/variables';
$theme-color-addons: (
'alt': $alt,
'gray': $gray-400,
);
// Merge/modify bootstrap variables.
$theme-colors: map-merge($theme-colors, $theme-color-addons);
$light: $gray-100;
$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");

View File

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

View File

@ -22,7 +22,10 @@
</script>
{% block head %}{% endblock %}
</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 javascript %}{% endblock %}
{% include './messages.html' %}
@ -30,4 +33,5 @@
{% block data %}{% endblock %}
</div>
</body>
{% endwith %}
</html>

View File

@ -13,7 +13,7 @@
</thead>
<tbody>
{% 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>
<td>{{ change.get_action_display|bettertitle }}</td>
<td>{{ change.changed_object_type.name|bettertitle }}</td>

View File

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

View File

@ -7,12 +7,12 @@
<div class="row">
<nav
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">
<a class="px-2 sidebar-logo" href="{% url 'home' %}">
{% load static %}
<img src="{% static 'netbox_logo.svg' %}" height="50" />
{% include 'logo.html' %}
</a>
<ul class="nav flex-column">
{% load nav %} {% nav %}
@ -22,7 +22,7 @@
</nav>
<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
type="button"
aria-expanded="false"

View File

@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 320" height="50">
<g id="netbox-logo-1">
<circle cx="37" cy="284" r="23"/>
<circle cx="101" cy="37" r="23"/>
<circle cx="101" cy="220" r="23"/>
<circle cx="284" cy="220" r="23"/>
<rect x="93" y="37" width="16" height="180"/>
<rect x="101" y="212" width="180" height="16"/>
<rect x="93" y="212" width="16" height="90" transform="rotate(45 101 220)"/>
</g>
<g id="netbox-logo-2">
<circle cx="284" cy="37" r="23"/>
<circle cx="37" cy="101" r="23"/>
<circle cx="220" cy="101" r="23"/>
<circle cx="220" cy="284" r="23"/>
<rect x="37" y="93" width="180" height="16"/>
<rect x="212" y="101" width="16" height="180"/>
<rect x="212" y="93" width="16" height="90" transform="rotate(225 220 101)"/>
<path transform="translate(380, 8)" d="M13.60 200L13.60 104L36.40 104L36.40 119.40L36.80 119.40Q40.20 112.20 47.20 106.90Q54.20 101.60 66.20 101.60L66.20 101.60Q75.80 101.60 82.50 104.80Q89.20 108 93.40 113.20Q97.60 118.40 99.40 125.20Q101.20 132 101.20 139.40L101.20 139.40L101.20 200L77.20 200L77.20 151.40Q77.20 147.40 76.80 142.50Q76.40 137.60 74.70 133.30Q73 129 69.40 126.10Q65.80 123.20 59.60 123.20L59.60 123.20Q53.60 123.20 49.50 125.20Q45.40 127.20 42.70 130.60Q40 134 38.80 138.40Q37.60 142.80 37.60 147.60L37.60 147.60L37.60 200L13.60 200ZM224.80 160.40L151.60 160.40Q152.80 171.20 160 177.20Q167.20 183.20 177.40 183.20L177.40 183.20Q186.40 183.20 192.50 179.50Q198.60 175.80 203.20 170.20L203.20 170.20L220.40 183.20Q212 193.60 201.60 198Q191.20 202.40 179.80 202.40L179.80 202.40Q169 202.40 159.40 198.80Q149.80 195.20 142.80 188.60Q135.80 182 131.70 172.70Q127.60 163.40 127.60 152L127.60 152Q127.60 140.60 131.70 131.30Q135.80 122 142.80 115.40Q149.80 108.80 159.40 105.20Q169 101.60 179.80 101.60L179.80 101.60Q189.80 101.60 198.10 105.10Q206.40 108.60 212.30 115.20Q218.20 121.80 221.50 131.50Q224.80 141.20 224.80 153.80L224.80 153.80L224.80 160.40ZM151.60 142.40L200.80 142.40Q200.60 131.80 194.20 125.70Q187.80 119.60 176.40 119.60L176.40 119.60Q165.60 119.60 159.30 125.80Q153 132 151.60 142.40L151.60 142.40ZM259.80 124.40L240.00 124.40L240.00 104L259.80 104L259.80 76.20L283.80 76.20L283.80 104L310.20 104L310.20 124.40L283.80 124.40L283.80 166.40Q283.80 173.60 286.50 177.80Q289.20 182 297.20 182L297.20 182Q300.40 182 304.20 181.30Q308 180.60 310.20 179L310.20 179L310.20 199.20Q306.40 201 300.90 201.70Q295.40 202.40 291.20 202.40L291.20 202.40Q281.60 202.40 275.50 200.30Q269.40 198.20 265.90 193.90Q262.40 189.60 261.10 183.20Q259.80 176.80 259.80 168.40L259.80 168.40L259.80 124.40ZM333.20 200L333.20 48.80L357.20 48.80L357.20 116.20L357.80 116.20Q359.60 113.80 362.40 111.30Q365.20 108.80 369.20 106.60Q373.20 104.40 378.40 103Q383.60 101.60 390.40 101.60L390.40 101.60Q400.60 101.60 409.20 105.50Q417.80 109.40 423.90 116.20Q430 123 433.40 132.20Q436.80 141.40 436.80 152L436.80 152Q436.80 162.60 433.60 171.80Q430.40 181 424.20 187.80Q418 194.60 409.20 198.50Q400.40 202.40 389.40 202.40L389.40 202.40Q379.20 202.40 370.40 198.40Q361.60 194.40 356.40 185.60L356.40 185.60L356 185.60L356 200L333.20 200ZM412.80 152L412.80 152Q412.80 146.40 410.90 141.20Q409 136 405.30 132Q401.60 128 396.40 125.60Q391.20 123.20 384.60 123.20L384.60 123.20Q378 123.20 372.80 125.60Q367.60 128 363.90 132Q360.20 136 358.30 141.20Q356.40 146.40 356.40 152L356.40 152Q356.40 157.60 358.30 162.80Q360.20 168 363.90 172Q367.60 176 372.80 178.40Q378 180.80 384.60 180.80L384.60 180.80Q391.20 180.80 396.40 178.40Q401.60 176 405.30 172Q409 168 410.90 162.80Q412.80 157.60 412.80 152ZM458.40 152L458.40 152Q458.40 140.60 462.50 131.30Q466.60 122 473.60 115.40Q480.60 108.80 490.20 105.20Q499.80 101.60 510.60 101.60L510.60 101.60Q521.40 101.60 531 105.20Q540.60 108.80 547.60 115.40Q554.60 122 558.70 131.30Q562.80 140.60 562.80 152L562.80 152Q562.80 163.40 558.70 172.70Q554.60 182 547.60 188.60Q540.60 195.20 531 198.80Q521.40 202.40 510.60 202.40L510.60 202.40Q499.80 202.40 490.20 198.80Q480.60 195.20 473.60 188.60Q466.60 182 462.50 172.70Q458.40 163.40 458.40 152ZM482.40 152L482.40 152Q482.40 157.60 484.30 162.80Q486.20 168 489.90 172Q493.60 176 498.80 178.40Q504 180.80 510.60 180.80L510.60 180.80Q517.20 180.80 522.40 178.40Q527.60 176 531.30 172Q535 168 536.90 162.80Q538.80 157.60 538.80 152L538.80 152Q538.80 146.40 536.90 141.20Q535 136 531.30 132Q527.60 128 522.40 125.60Q517.20 123.20 510.60 123.20L510.60 123.20Q504 123.20 498.80 125.60Q493.60 128 489.90 132Q486.20 136 484.30 141.20Q482.40 146.40 482.40 152ZM575.40 200L614 148.40L580.80 104L610 104L629.20 132.80L650 104L677.40 104L644.60 148.40L683.20 200L654 200L629 165.60L603.80 200L575.40 200Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -10,21 +10,27 @@
<span id="navbar_user">{{ request.user|truncatechars:"30" }}</span>
</button>
<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 %}
<a class="text-decoration-none" href="{% url 'admin:index' %}">
<a class="dropdown-item" href="{% url 'admin:index' %}">
<i class="mdi mdi-cog"></i> Admin
</a>
{% endif %}
</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
</a>
</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
</a>
</li>

View File

@ -4,32 +4,56 @@
{% block title %}User Preferences{% endblock %}
{% 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>
</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 %}

View File

@ -91,6 +91,7 @@ class LogoutView(View):
"""
Deauthenticate a web user.
"""
def get(self, request):
logger = logging.getLogger('netbox.auth.logout')
@ -136,9 +137,17 @@ class UserConfigView(LoginRequiredMixin, View):
data = userconfig.all()
# 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()
messages.success(request, "Your preferences have been updated.")