mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 04:56:29 -06:00
Merge feature
This commit is contained in:
commit
3ba122afd4
@ -5,6 +5,8 @@
|
||||
### Bug Fixes
|
||||
|
||||
* [#6811](https://github.com/netbox-community/netbox/issues/6811) - Fix exception when editing users
|
||||
* [#6827](https://github.com/netbox-community/netbox/issues/6827) - Fix circuit termination connection dropdown
|
||||
* [#6846](https://github.com/netbox-community/netbox/issues/6846) - Form-driven REST API calls should use brief mode
|
||||
|
||||
---
|
||||
|
||||
|
BIN
netbox/project-static/dist/config.js.map
vendored
BIN
netbox/project-static/dist/config.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js
vendored
BIN
netbox/project-static/dist/jobs.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/jobs.js.map
vendored
BIN
netbox/project-static/dist/jobs.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/lldp.js.map
vendored
BIN
netbox/project-static/dist/lldp.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-dark.css
vendored
BIN
netbox/project-static/dist/netbox-dark.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-external.css
vendored
BIN
netbox/project-static/dist/netbox-external.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox-light.css
vendored
BIN
netbox/project-static/dist/netbox-light.css
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js
vendored
BIN
netbox/project-static/dist/status.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/status.js.map
vendored
BIN
netbox/project-static/dist/status.js.map
vendored
Binary file not shown.
@ -41,6 +41,32 @@
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"prettier": "^2.2.1",
|
||||
"prettier-eslint": "^12.0.0",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-twbs-bootstrap": "^2.2.3",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"stylelint": {
|
||||
"extends": "stylelint-config-twbs-bootstrap/scss",
|
||||
"rules": {
|
||||
"selector-max-class": 16,
|
||||
"selector-max-compound-selectors": 16,
|
||||
"selector-no-qualifying-type": [
|
||||
true,
|
||||
{
|
||||
"ignore": [
|
||||
"attribute",
|
||||
"class"
|
||||
]
|
||||
}
|
||||
],
|
||||
"number-leading-zero": "always",
|
||||
"string-quotes": "single",
|
||||
"selector-pseudo-element-colon-notation": "single",
|
||||
"declaration-property-value-disallowed-list": {
|
||||
"border": "none",
|
||||
"outline": "none"
|
||||
},
|
||||
"scss/selector-no-union-class-name": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,9 +32,7 @@ function handleSearchDropdownClick(event: Event, button: HTMLButtonElement) {
|
||||
* Initialize Search Bar Elements.
|
||||
*/
|
||||
function initSearchBar() {
|
||||
for (const dropdown of getElements<HTMLUListElement>(
|
||||
'div.search-container ul.search-obj-selector',
|
||||
)) {
|
||||
for (const dropdown of getElements<HTMLUListElement>('.search-obj-selector')) {
|
||||
for (const button of dropdown.querySelectorAll<HTMLButtonElement>(
|
||||
'li > button.dropdown-item',
|
||||
)) {
|
||||
|
@ -123,6 +123,11 @@ class APISelect {
|
||||
*/
|
||||
private disabledOptions: Array<string> = [];
|
||||
|
||||
/**
|
||||
* Array of properties which if truthy on an API object should be considered disabled.
|
||||
*/
|
||||
private disabledAttributes: Array<string> = DISABLED_ATTRIBUTES;
|
||||
|
||||
constructor(base: HTMLSelectElement) {
|
||||
// Initialize readonly properties.
|
||||
this.base = base;
|
||||
@ -141,6 +146,7 @@ class APISelect {
|
||||
this.loadEvent = new Event(`netbox.select.onload.${base.name}`);
|
||||
this.placeholder = this.getPlaceholder();
|
||||
this.disabledOptions = this.getDisabledOptions();
|
||||
this.disabledAttributes = this.getDisabledAttributes();
|
||||
|
||||
this.slim = new SlimSelect({
|
||||
select: this.base,
|
||||
@ -158,12 +164,18 @@ class APISelect {
|
||||
this.updateQueryParams(filter);
|
||||
}
|
||||
|
||||
// Add any already-resolved key/value pairs to the API query parameters.
|
||||
for (const [key, value] of this.filterParams.entries()) {
|
||||
if (isTruthy(value)) {
|
||||
this.queryParams.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const filter of this.pathValues.keys()) {
|
||||
this.updatePathValues(filter);
|
||||
}
|
||||
|
||||
// TODO: Re-enable this. Disabled because `_depth` field is missing from brief responses.
|
||||
// this.queryParams.set('brief', true);
|
||||
this.queryParams.set('brief', true);
|
||||
this.queryParams.set('limit', 0);
|
||||
this.updateQueryUrl();
|
||||
|
||||
@ -323,7 +335,7 @@ class APISelect {
|
||||
if (!this.preSorted) {
|
||||
this.preSorted = true;
|
||||
}
|
||||
text = `<span class="depth">${'─'.repeat(result._depth)}</span> ${text}`;
|
||||
text = `<span class="depth">${'─'.repeat(result._depth)} </span>${text}`;
|
||||
}
|
||||
const data = {} as Record<string, string>;
|
||||
const value = result.id.toString();
|
||||
@ -336,7 +348,7 @@ class APISelect {
|
||||
data[key] = String(v);
|
||||
}
|
||||
// Set option to disabled if the result contains a matching key and is truthy.
|
||||
if (DISABLED_ATTRIBUTES.some(key => key.toLowerCase() === k.toLowerCase())) {
|
||||
if (this.disabledAttributes.some(key => key.toLowerCase() === k.toLowerCase())) {
|
||||
if (typeof v === 'string' && v.toLowerCase() !== 'false') {
|
||||
disabled = true;
|
||||
} else if (typeof v === 'boolean' && v === true) {
|
||||
@ -547,6 +559,19 @@ class APISelect {
|
||||
return disabledOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this element's disabled attribute keys. For example, if `disabled-indicator` is set to
|
||||
* `'_occupied'` and an API object contains `{ _occupied: true }`, the option will be disabled.
|
||||
*/
|
||||
private getDisabledAttributes(): string[] {
|
||||
let disabled = [...DISABLED_ATTRIBUTES] as string[];
|
||||
const attr = this.base.getAttribute('disabled-indicator');
|
||||
if (isTruthy(attr)) {
|
||||
disabled = [...disabled, attr];
|
||||
}
|
||||
return disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the `data-url` attribute to add any Django template variables to `pathValues` as keys
|
||||
* with empty values. As those keys' corresponding form fields' values change, `pathValues` will
|
||||
|
@ -1,22 +1,246 @@
|
||||
import { getElement, getElements } from './util';
|
||||
import { StateManager } from './state';
|
||||
import { getElements, isElement } from './util';
|
||||
|
||||
const breakpoints = {
|
||||
sm: 540,
|
||||
md: 720,
|
||||
lg: 960,
|
||||
xl: 1140,
|
||||
};
|
||||
type NavState = { pinned: boolean };
|
||||
type BodyAttr = 'show' | 'hide' | 'hidden' | 'pinned';
|
||||
|
||||
function toggleBodyPosition(position: HTMLBodyElement['style']['position']): void {
|
||||
for (const element of getElements('body')) {
|
||||
element.style.position = position;
|
||||
class SideNav {
|
||||
/**
|
||||
* Sidenav container element.
|
||||
*/
|
||||
private base: HTMLDivElement;
|
||||
|
||||
/**
|
||||
* SideNav internal state manager.
|
||||
*/
|
||||
private state: StateManager<NavState>;
|
||||
|
||||
constructor(base: HTMLDivElement) {
|
||||
this.base = base;
|
||||
this.state = new StateManager<NavState>({ pinned: true }, { persist: true });
|
||||
|
||||
this.init();
|
||||
this.initLinks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if `document.body` has a sidenav attribute.
|
||||
*/
|
||||
private bodyHas(attr: BodyAttr): boolean {
|
||||
return document.body.hasAttribute(`data-sidenav-${attr}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove sidenav attributes from `document.body`.
|
||||
*/
|
||||
private bodyRemove(...attrs: BodyAttr[]): void {
|
||||
for (const attr of attrs) {
|
||||
document.body.removeAttribute(`data-sidenav-${attr}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add sidenav attributes to `document.body`.
|
||||
*/
|
||||
private bodyAdd(...attrs: BodyAttr[]): void {
|
||||
for (const attr of attrs) {
|
||||
document.body.setAttribute(`data-sidenav-${attr}`, '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial values & add event listeners.
|
||||
*/
|
||||
private init() {
|
||||
for (const toggler of this.base.querySelectorAll('.sidenav-toggle')) {
|
||||
toggler.addEventListener('click', event => this.onToggle(event));
|
||||
}
|
||||
|
||||
for (const toggler of getElements<HTMLButtonElement>('.sidenav-toggle-mobile')) {
|
||||
toggler.addEventListener('click', event => this.onMobileToggle(event));
|
||||
}
|
||||
|
||||
if (window.innerWidth > 1200) {
|
||||
if (this.state.get('pinned')) {
|
||||
this.pin();
|
||||
}
|
||||
|
||||
if (!this.state.get('pinned')) {
|
||||
this.unpin();
|
||||
}
|
||||
window.addEventListener('resize', () => this.onResize());
|
||||
}
|
||||
|
||||
if (window.innerWidth < 1200) {
|
||||
this.bodyRemove('hide');
|
||||
this.bodyAdd('hidden');
|
||||
window.addEventListener('resize', () => this.onResize());
|
||||
}
|
||||
|
||||
this.base.addEventListener('mouseenter', () => this.onEnter());
|
||||
this.base.addEventListener('mouseleave', () => this.onLeave());
|
||||
}
|
||||
|
||||
/**
|
||||
* If the sidenav is shown, expand active nav links. Otherwise, collapse them.
|
||||
*/
|
||||
private initLinks(): void {
|
||||
for (const link of this.getActiveLinks()) {
|
||||
if (this.bodyHas('show')) {
|
||||
this.activateLink(link, 'expand');
|
||||
} else if (this.bodyHas('hidden')) {
|
||||
this.activateLink(link, 'collapse');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private show(): void {
|
||||
this.bodyAdd('show');
|
||||
this.bodyRemove('hidden', 'hide');
|
||||
}
|
||||
|
||||
private hide(): void {
|
||||
this.bodyAdd('hidden');
|
||||
this.bodyRemove('pinned', 'show');
|
||||
for (const collapse of this.base.querySelectorAll('.collapse')) {
|
||||
collapse.classList.remove('show');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin the sidenav.
|
||||
*/
|
||||
private pin(): void {
|
||||
this.bodyAdd('show', 'pinned');
|
||||
this.bodyRemove('hidden');
|
||||
this.state.set('pinned', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin the sidenav.
|
||||
*/
|
||||
private unpin(): void {
|
||||
this.bodyRemove('pinned', 'show');
|
||||
this.bodyAdd('hidden');
|
||||
for (const collapse of this.base.querySelectorAll('.collapse')) {
|
||||
collapse.classList.remove('show');
|
||||
}
|
||||
this.state.set('pinned', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from the bottom-most active link in the element tree, work backwards to determine the
|
||||
* link's containing `.collapse` element and the `.collapse` element's containing `.nav-link`
|
||||
* element. Once found, expand (or collapse) the `.collapse` element and add (or remove) the
|
||||
* `.active` class to the the parent `.nav-link` element.
|
||||
*
|
||||
* @param link Active nav link
|
||||
* @param action Expand or Collapse
|
||||
*/
|
||||
private activateLink(link: HTMLAnchorElement, action: 'expand' | 'collapse'): void {
|
||||
// Find the closest .collapse element, which should contain `link`.
|
||||
const collapse = link.closest('.collapse') as Nullable<HTMLDivElement>;
|
||||
if (isElement(collapse)) {
|
||||
// Find the closest `.nav-link`, which should be adjacent to the `.collapse` element.
|
||||
const groupLink = collapse.parentElement?.querySelector('.nav-link');
|
||||
if (isElement(groupLink)) {
|
||||
groupLink.classList.add('active');
|
||||
switch (action) {
|
||||
case 'expand':
|
||||
groupLink.setAttribute('aria-expanded', 'true');
|
||||
collapse.classList.add('show');
|
||||
link.classList.add('active');
|
||||
break;
|
||||
case 'collapse':
|
||||
groupLink.setAttribute('aria-expanded', 'false');
|
||||
collapse.classList.remove('show');
|
||||
link.classList.remove('active');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any nav links with `href` attributes matching the current path, to determine which nav
|
||||
* link should be considered active.
|
||||
*/
|
||||
private *getActiveLinks(): Generator<HTMLAnchorElement> {
|
||||
for (const link of this.base.querySelectorAll<HTMLAnchorElement>(
|
||||
'.navbar-nav .nav .nav-item a.nav-link',
|
||||
)) {
|
||||
const href = new RegExp(link.href, 'gi');
|
||||
if (Boolean(window.location.href.match(href))) {
|
||||
yield link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the sidenav and expand any active sections.
|
||||
*/
|
||||
private onEnter(): void {
|
||||
if (!this.bodyHas('pinned')) {
|
||||
this.bodyRemove('hide', 'hidden');
|
||||
this.bodyAdd('show');
|
||||
for (const link of this.getActiveLinks()) {
|
||||
this.activateLink(link, 'expand');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the sidenav and collapse any active sections.
|
||||
*/
|
||||
private onLeave(): void {
|
||||
if (!this.bodyHas('pinned')) {
|
||||
this.bodyRemove('show');
|
||||
this.bodyAdd('hide');
|
||||
for (const link of this.getActiveLinks()) {
|
||||
this.activateLink(link, 'collapse');
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.bodyRemove('hide');
|
||||
this.bodyAdd('hidden');
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the (unpinned) sidenav when the window is resized.
|
||||
*/
|
||||
private onResize(): void {
|
||||
if (this.bodyHas('show') && !this.bodyHas('pinned')) {
|
||||
this.bodyRemove('show');
|
||||
this.bodyAdd('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin & unpin the sidenav when the pin button is toggled.
|
||||
*/
|
||||
private onToggle(event: Event): void {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.state.get('pinned')) {
|
||||
this.unpin();
|
||||
} else {
|
||||
this.pin();
|
||||
}
|
||||
}
|
||||
|
||||
private onMobileToggle(event: Event): void {
|
||||
event.preventDefault();
|
||||
if (this.bodyHas('hidden')) {
|
||||
this.show();
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function initSideNav() {
|
||||
const element = getElement<HTMLAnchorElement>('sidebarMenu');
|
||||
if (element !== null && document.body.clientWidth < breakpoints.lg) {
|
||||
element.addEventListener('shown.bs.collapse', () => toggleBodyPosition('fixed'));
|
||||
element.addEventListener('hidden.bs.collapse', () => toggleBodyPosition('relative'));
|
||||
for (const sidenav of getElements<HTMLDivElement>('.sidenav')) {
|
||||
new SideNav(sidenav);
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,13 @@ export function isTruthy<V extends string | number | boolean | null | undefined>
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to determine if a value is an `Element`.
|
||||
*/
|
||||
export function isElement(obj: Element | null | undefined): obj is Element {
|
||||
return typeof obj !== null && typeof obj !== 'undefined';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the CSRF token from cookie storage.
|
||||
*/
|
||||
@ -152,6 +159,22 @@ export function getElement<E extends HTMLElement>(id: string): Nullable<E> {
|
||||
return document.getElementById(id) as Nullable<E>;
|
||||
}
|
||||
|
||||
export function removeElements(...selectors: string[]): void {
|
||||
for (const element of getElements(...selectors)) {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function elementWidth<E extends HTMLElement>(element: Nullable<E>): number {
|
||||
let width = 0;
|
||||
if (element !== null) {
|
||||
const style = getComputedStyle(element);
|
||||
const pre = style.width.replace('px', '');
|
||||
width = parseFloat(pre);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* scrollTo() wrapper that calculates a Y offset relative to `element`, but also factors in an
|
||||
* offset relative to div#content-title. This ensures we scroll to the element, but leave enough
|
||||
|
@ -2,3 +2,4 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
@import '../node_modules/@mdi/font/css/materialdesignicons.min.css';
|
||||
@import '../node_modules/flatpickr/dist/flatpickr.css';
|
||||
@import '../node_modules/simplebar/dist/simplebar.css';
|
||||
|
@ -1,9 +1,11 @@
|
||||
// Netbox-specific Styles and Overrides.
|
||||
|
||||
@use 'sass:map';
|
||||
@import './sidenav.scss';
|
||||
|
||||
:root {
|
||||
--nbx-sidebar-bg: #{$gray-200};
|
||||
--nbx-sidebar-scroll: #{$gray-500};
|
||||
--nbx-sidebar-link-color: #{$gray-800};
|
||||
--nbx-sidebar-link-hover-bg: #{$blue-100};
|
||||
--nbx-sidebar-title-color: #{$text-muted};
|
||||
@ -21,9 +23,11 @@
|
||||
--nbx-cable-termination-border-color: #{$gray-300};
|
||||
--nbx-search-filter-border-left-color: #{$gray-300};
|
||||
--nbx-color-mode-toggle-color: #{$primary};
|
||||
--nbx-sidenav-pin-color: #{$orange};
|
||||
|
||||
&[data-netbox-color-mode='dark'] {
|
||||
--nbx-sidebar-bg: #{$gray-900};
|
||||
--nbx-sidebar-scroll: #{$gray-700};
|
||||
--nbx-sidebar-link-color: #{$gray-100};
|
||||
--nbx-sidebar-link-hover-bg: #{rgba($blue-300, 0.15)};
|
||||
--nbx-sidebar-title-color: #{$gray-600};
|
||||
@ -41,6 +45,7 @@
|
||||
--nbx-cable-termination-border-color: #{$gray-700};
|
||||
--nbx-search-filter-border-left-color: #{$gray-600};
|
||||
--nbx-color-mode-toggle-color: #{$yellow-300};
|
||||
--nbx-sidenav-pin-color: #{$yellow};
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,6 +124,13 @@ small {
|
||||
background: transparent escape-svg($btn-close-bg) center / $btn-close-width auto no-repeat;
|
||||
}
|
||||
|
||||
.btn.btn-ghost-#{$color} {
|
||||
color: $value;
|
||||
&:hover {
|
||||
background-color: rgba($value, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
// Use Bootstrap's method of coloring the .alert-link class automatically.
|
||||
// See: https://github.com/twbs/bootstrap/blob/2bdbb42dcf6bfb99b5e9e5444d9e64589eb8c08f/scss/_alert.scss#L50-L52
|
||||
.toast.bg-#{$color},
|
||||
@ -160,6 +172,34 @@ table td > .progress {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.nav-mobile {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-mobile-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card > .table.table-flush {
|
||||
margin-bottom: 0;
|
||||
overflow: hidden;
|
||||
@ -359,6 +399,8 @@ div.title-container {
|
||||
|
||||
nav.search {
|
||||
background-color: var(--nbx-body-bg);
|
||||
// Don't overtake dropdowns
|
||||
z-index: 999;
|
||||
form button.dropdown-toggle {
|
||||
border-color: $input-border-color;
|
||||
font-weight: $input-group-addon-font-weight;
|
||||
@ -374,6 +416,16 @@ nav.search {
|
||||
}
|
||||
}
|
||||
|
||||
main.layout {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
height: 100vh;
|
||||
height: -webkit-fill-available;
|
||||
max-height: 100vh;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
main.login-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 4rem);
|
||||
@ -390,6 +442,18 @@ main.login-container {
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding-top: map.get($spacers, 4);
|
||||
padding-right: 0;
|
||||
padding-bottom: map.get($spacers, 3);
|
||||
padding-left: 0;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
// Pad the bottom of the footer on mobile devices to account for mobile browser controls.
|
||||
margin-bottom: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
footer.login-footer {
|
||||
height: 4rem;
|
||||
margin-top: auto;
|
||||
@ -425,7 +489,6 @@ h3.accordion-item-title,
|
||||
h4.accordion-item-title,
|
||||
h5.accordion-item-title,
|
||||
h6.accordion-item-title {
|
||||
// padding: 0 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-weight: $font-weight-bold;
|
||||
text-transform: uppercase;
|
||||
@ -474,7 +537,7 @@ li.dropdown-item.dropdown-item-btns {
|
||||
height: calc(100vh - 48px);
|
||||
padding-top: 0.5rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
|
||||
overflow-y: auto; // Scrollable contents if viewport is shorter than content.
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
@ -498,13 +561,16 @@ nav.nav.nav-pills {
|
||||
// Ensure the content container is full-height, and that the content block is also full height so
|
||||
// that the footer is always at the bottom.
|
||||
div.content-container {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 4.5rem);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-left: $sidebar-width;
|
||||
@include media-breakpoint-down(lg) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.content {
|
||||
@ -527,7 +593,7 @@ div.content-container {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 100; /* Behind the navbar */
|
||||
z-index: 100; // Behind the navbar
|
||||
border-right: 1px solid $border-color;
|
||||
background-color: var(--nbx-sidebar-bg);
|
||||
max-height: 100%;
|
||||
@ -632,6 +698,11 @@ div.content-container {
|
||||
}
|
||||
|
||||
.search-obj-selector {
|
||||
@include media-breakpoint-down(lg) {
|
||||
// Limit the height and enable scrolling on mobile devices.
|
||||
max-height: 75vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dropdown-item,
|
||||
.dropdown-header {
|
||||
font-size: $font-size-sm;
|
||||
@ -807,7 +878,7 @@ div.field-group:not(:first-of-type) {
|
||||
|
||||
label.required {
|
||||
font-weight: $font-weight-bold;
|
||||
&::after {
|
||||
&:after {
|
||||
font-family: 'Material Design Icons';
|
||||
content: '\f06C4';
|
||||
font-weight: normal;
|
||||
@ -827,29 +898,34 @@ div.bulk-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: $spacer / 2 0;
|
||||
|
||||
// Each group of buttons needs to be contained separately for alignment purposes. This way, you
|
||||
// can put some buttons in a group that aligns left, and other buttons in a group that aligns
|
||||
// right.
|
||||
& > div.bulk-button-group {
|
||||
> div.bulk-button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&:first-of-type:not(:last-of-type) {
|
||||
// If there are multiple bulk button groups and this is the first, the first button in the
|
||||
// group should *not* have left spacing applied, so the button group aligns with the rest
|
||||
// of the page elements.
|
||||
& > *:first-child {
|
||||
> *:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type:not(:first-of-type) {
|
||||
// If there are multiple bulk button groups and this is the last, the last button in the
|
||||
// group should *not* have right spacing applied, so the button group aligns with the rest
|
||||
// of the page elements.
|
||||
& > *:last-child {
|
||||
> *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// However, the rest of the buttons should have spacing applied in all directions.
|
||||
& > * {
|
||||
> * {
|
||||
margin: $spacer / 4;
|
||||
}
|
||||
}
|
||||
@ -963,12 +1039,12 @@ html {
|
||||
&[data-netbox-path='/'] {
|
||||
.content-container,
|
||||
.search {
|
||||
background-color: $gray-100;
|
||||
background-color: $gray-100 !important;
|
||||
}
|
||||
&[data-netbox-color-mode='dark'] {
|
||||
.content-container,
|
||||
.search {
|
||||
background-color: $darkest;
|
||||
background-color: $darkest !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,10 @@ $spacing-s: $input-padding-x;
|
||||
border-color: currentColor;
|
||||
}
|
||||
}
|
||||
.placeholder .depth {
|
||||
// Don't show the depth indicator outside of the menu.
|
||||
display: none;
|
||||
}
|
||||
span.placeholder > *,
|
||||
span.placeholder {
|
||||
line-height: $input-line-height;
|
||||
|
420
netbox/project-static/styles/sidenav.scss
Normal file
420
netbox/project-static/styles/sidenav.scss
Normal file
@ -0,0 +1,420 @@
|
||||
@use 'sass:map';
|
||||
|
||||
@mixin parent-link {
|
||||
.navbar-nav .nav-item .nav-link[data-bs-toggle] {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin child-link {
|
||||
.collapse .nav .nav-item .nav-link {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sidenav-open {
|
||||
body[data-sidenav-show],
|
||||
body[data-sidenav-pinned] {
|
||||
.sidenav {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sidenav-closed {
|
||||
body[data-sidenav-hide],
|
||||
body[data-sidenav-hidden] {
|
||||
.sidenav {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sidenav-pinned {
|
||||
body[data-sidenav-pinned] {
|
||||
.sidenav {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sidenav-show {
|
||||
body[data-sidenav-show] {
|
||||
.sidenav {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sidenav-hide {
|
||||
body[data-sidenav-hide] {
|
||||
.sidenav {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin sidenav-peek {
|
||||
.g-sidenav-show:not(.g-sidenav-pinned) {
|
||||
.sidenav {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$transition-100ms-ease-in-out: all 0.1s ease-in-out;
|
||||
|
||||
.sidenav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1050;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: $sidenav-width-closed;
|
||||
padding-top: 0;
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
background-color: var(--nbx-sidebar-bg);
|
||||
border-right: 1px solid $border-color;
|
||||
transition: $transition-100ms-ease-in-out;
|
||||
|
||||
// Media fixes for mobile resolutions.
|
||||
@include media-breakpoint-down(lg) {
|
||||
transform: translateX(-$sidenav-width-closed);
|
||||
|
||||
+ .content-container[class] {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.profile-button-container[class] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-button-container {
|
||||
display: none;
|
||||
padding: $sidenav-link-spacing-y $sidenav-link-spacing-x;
|
||||
}
|
||||
|
||||
+ .content-container {
|
||||
margin-left: $sidenav-width-closed;
|
||||
transition: $transition-100ms-ease-in-out;
|
||||
}
|
||||
|
||||
// Navbar brand
|
||||
.sidenav-brand {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.sidenav-inner {
|
||||
padding-right: $sidenav-spacing-x;
|
||||
padding-left: $sidenav-spacing-x;
|
||||
@include media-breakpoint-up(md) {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav-brand-img,
|
||||
.sidenav-brand > img {
|
||||
max-width: 100%;
|
||||
max-height: calc(#{$sidenav-width-open} - 1rem);
|
||||
}
|
||||
|
||||
.navbar-heading {
|
||||
padding-top: $nav-link-padding-y;
|
||||
padding-bottom: $nav-link-padding-y;
|
||||
font-size: $font-size-xs;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.sidenav-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 78px;
|
||||
padding: $spacer;
|
||||
transition: $transition-100ms-ease-in-out;
|
||||
}
|
||||
|
||||
.sidenav-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidenav-collapse {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding-right: $sidenav-spacing-x;
|
||||
padding-left: $sidenav-spacing-x;
|
||||
margin-right: -$sidenav-spacing-x;
|
||||
margin-left: -$sidenav-spacing-x;
|
||||
|
||||
> * {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Child Link nav-item
|
||||
.nav .nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include child-link() {
|
||||
width: 100%;
|
||||
padding-top: $sidenav-link-spacing-y / 2.675;
|
||||
padding-right: map.get($spacers, 1);
|
||||
padding-bottom: $sidenav-link-spacing-y / 2.675;
|
||||
/* stylelint-disable */
|
||||
padding-left: $sidenav-link-spacing-x + $sidenav-icon-width + $sidenav-link-spacing-x / 4;
|
||||
/* stylelint-enable */
|
||||
margin-top: $sidenav-link-spacing-y / 3.3;
|
||||
margin-bottom: $sidenav-link-spacing-y / 3.3;
|
||||
font-size: $font-size-xs;
|
||||
border-top-right-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
|
||||
.sidenav-normal {
|
||||
color: $text-muted;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.sidenav-mini-icon {
|
||||
width: $sidenav-link-spacing-x;
|
||||
text-align: center;
|
||||
transition: $transition-100ms-ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@include parent-link() {
|
||||
width: unset;
|
||||
height: 100%;
|
||||
font-weight: $font-weight-bold;
|
||||
|
||||
&.active {
|
||||
color: $accordion-button-active-color;
|
||||
background: $accordion-button-active-bg;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: inline-block;
|
||||
margin-left: auto;
|
||||
/* stylelint-disable */
|
||||
font-family: 'Material Design Icons';
|
||||
/* stylelint-enable */
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-variant: normal;
|
||||
color: $text-muted;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
content: '\f0142';
|
||||
transition: $transition-100ms-ease-in-out;
|
||||
}
|
||||
|
||||
// Expanded
|
||||
&[aria-expanded='true'] {
|
||||
&.active:after {
|
||||
color: $accordion-button-active-color;
|
||||
}
|
||||
&:after {
|
||||
color: $primary;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link-text {
|
||||
padding-left: 0.25rem;
|
||||
transition: $transition-100ms-ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
flex-direction: column;
|
||||
margin-right: -$sidenav-spacing-x;
|
||||
margin-left: -$sidenav-spacing-x;
|
||||
|
||||
.nav-item {
|
||||
margin-top: 2px;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
// All Links
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: $sidenav-link-spacing-y $sidenav-link-spacing-x;
|
||||
font-size: $font-size-sm;
|
||||
white-space: nowrap;
|
||||
transition: $transition-100ms-ease-in-out;
|
||||
|
||||
// &.disabled {
|
||||
// opacity: 0.8;
|
||||
// }
|
||||
|
||||
&.active {
|
||||
position: relative;
|
||||
color: var(--nbx-sidebar-link-hover-bg);
|
||||
background-color: var(--nbx-sidebar-link-hover-bg);
|
||||
}
|
||||
|
||||
// Icon
|
||||
> i {
|
||||
min-width: $sidenav-icon-width;
|
||||
font-size: calc(45px / 2);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-group-label {
|
||||
display: block;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $primary;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include sidenav-pinned() {
|
||||
.sidenav-toggle-icon {
|
||||
color: var(--nbx-sidenav-pin-color);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
+ .content-container {
|
||||
margin-left: $sidenav-width-open;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include sidenav-peek() {
|
||||
.sidenav-toggle-icon {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@include sidenav-open() {
|
||||
max-width: $sidenav-width-open;
|
||||
|
||||
.sidenav-brand,
|
||||
.navbar-heading {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidenav-brand {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidenav-brand-icon {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@include sidenav-closed() {
|
||||
.sidenav-header {
|
||||
padding: $spacer * 0.5;
|
||||
}
|
||||
|
||||
.sidenav-brand {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transform: translateX(-150%);
|
||||
}
|
||||
|
||||
.sidenav-brand-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.navbar-nav > .nav-item {
|
||||
> .nav-link {
|
||||
&:after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item .collapse {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-link-text {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include parent-link() {
|
||||
&.active {
|
||||
margin-right: 0;
|
||||
margin-left: 0;
|
||||
border-radius: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include sidenav-show() {
|
||||
.sidenav-brand {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-item .collapse {
|
||||
height: auto;
|
||||
transition: $transition-100ms-ease-in-out;
|
||||
}
|
||||
|
||||
.nav-item .nav-link .nav-link-text {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-item .sidenav-mini-icon {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.sidenav-toggle {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.simplebar-track.simplebar-vertical {
|
||||
right: 0;
|
||||
width: 6px;
|
||||
background-color: transparent;
|
||||
|
||||
.simplebar-scrollbar:before {
|
||||
right: 0;
|
||||
width: 3px;
|
||||
background: var(--nbx-sidebar-scroll);
|
||||
border-radius: $border-radius;
|
||||
}
|
||||
&.simplebar-hover .simplebar-scrollbar:before {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
@ -122,11 +122,13 @@ $theme-color-addons: (
|
||||
'pink-900': $pink-900,
|
||||
);
|
||||
|
||||
/* stylelint-disable */
|
||||
$font-family-sans-serif: Inter, 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;
|
||||
/* stylelint-enable */
|
||||
|
||||
// This is the same value as the default from Bootstrap, but it needs to be in scope prior to
|
||||
// importing _variables.scss from Bootstrap.
|
||||
@ -137,3 +139,12 @@ $accordion-padding-x: 0.8125rem;
|
||||
|
||||
$sidebar-width: 280px;
|
||||
$sidebar-bottom-height: 4rem;
|
||||
|
||||
// Sidebar/Sidenav
|
||||
$sidenav-width-closed: 4rem;
|
||||
$sidenav-width-open: 16rem;
|
||||
$sidenav-icon-width: 2rem;
|
||||
$sidenav-link-px: 1rem;
|
||||
$sidenav-spacing-x: 1.5rem;
|
||||
$sidenav-link-spacing-x: 1rem;
|
||||
$sidenav-link-spacing-y: 0.675rem;
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Dark Mode Theme Variables and Overrides.
|
||||
|
||||
@use 'sass:map';
|
||||
@import './theme-base.scss';
|
||||
@import './theme-base';
|
||||
|
||||
$primary: $blue-300;
|
||||
$secondary: $gray-500;
|
||||
@ -175,13 +175,11 @@ $accordion-bg: transparent;
|
||||
$accordion-border-color: $border-color;
|
||||
$accordion-button-color: $accordion-color;
|
||||
$accordion-button-bg: $accordion-bg;
|
||||
$accordion-body-active-bg: rgba($blue-300, 0.2);
|
||||
$accordion-button-active-bg: rgba($blue-300, 0.25);
|
||||
$accordion-button-active-color: $gray-300;
|
||||
$accordion-button-active-bg: shade-color($blue-300, 10%);
|
||||
$accordion-button-active-color: color-contrast($accordion-button-active-bg);
|
||||
$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>");
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"
|
||||
content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width, viewport-fit=cover"
|
||||
/>
|
||||
|
||||
{# Page title #}
|
||||
|
@ -6,103 +6,51 @@
|
||||
{% load static %}
|
||||
|
||||
{% block layout %}
|
||||
|
||||
<div class="container-fluid px-0">
|
||||
<main class="ms-sm-auto">
|
||||
<main class="layout">
|
||||
{# Sidebar #}
|
||||
<nav id="sidebar-menu" class="d-md-block sidebar collapse px-0" data-simplebar>
|
||||
|
||||
{# Sidebar content #}
|
||||
<div class="position-sticky">
|
||||
|
||||
{# Logo #}
|
||||
<div class="py-2">
|
||||
<a class="sidebar-logo d-none d-md-flex justify-content-center" href="{% url 'home' %}">
|
||||
<img src="{% static 'netbox_logo.svg' %}" alt="NetBox logo" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<ul class="nav flex-column">
|
||||
|
||||
{# Search bar for collapsed menu #}
|
||||
<div class="d-block d-md-none mx-1 my-3 search-container">
|
||||
{% search_options %}
|
||||
</div>
|
||||
<div class="d-flex d-md-none mx-1 my-3 justify-content-center justify-content-md-end order-last order-md-0">
|
||||
{% include 'inc/profile_button.html' %}
|
||||
</div>
|
||||
|
||||
{# Navigation menu #}
|
||||
{% nav %}
|
||||
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
{# Sidebar footer #}
|
||||
<div class="d-flex flex-column container-fluid mt-auto justify-content-end sidebar-bottom">
|
||||
<nav class="nav">
|
||||
|
||||
{# Documentation #}
|
||||
<a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank">
|
||||
<i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
|
||||
{# REST API #}
|
||||
<a type="button" class="nav-link" href="{% url 'api-root' %}" target="_blank">
|
||||
<i title="REST API" class="mdi mdi-cloud-braces text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
|
||||
{# API docs #}
|
||||
<a type="button" class="nav-link" href="{% url 'api_docs' %}" target="_blank">
|
||||
<i title="REST API documentation" class="mdi mdi-book text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
|
||||
{# GraphQL API #}
|
||||
{% if settings.GRAPHQL_ENABLED %}
|
||||
<a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank">
|
||||
<i title="GraphQL API" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# GitHub #}
|
||||
<a type="button" class="nav-link" href="https://github.com/netbox-community/netbox" target="_blank">
|
||||
<i title="Source Code" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
|
||||
{# NetDev Slack #}
|
||||
<a type="button" class="nav-link" href="https://netdev.chat/" target="_blank">
|
||||
<i title="Community" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
{% include 'base/sidenav.html' %}
|
||||
|
||||
{# Body #}
|
||||
<div class="content-container">
|
||||
|
||||
{# Top bar #}
|
||||
<nav class="navbar navbar-light sticky-top flex-md-nowrap p-3 search container-fluid">
|
||||
<div class="d-md-none w-100 d-flex justify-content-between align-items-center my-3">
|
||||
<a class="p-2 sidebar-logo d-block d-md-none" href="{% url 'home' %}">
|
||||
<img src="{% static 'netbox_logo.svg' %}" alt="NetBox logo" width="100%" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
data-bs-toggle="collapse"
|
||||
aria-controls="sidebar-menu"
|
||||
data-bs-target="#sidebar-menu"
|
||||
aria-label="Toggle Navigation"
|
||||
class="navbar-toggler position-relative collapsed"
|
||||
>
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<nav class="navbar navbar-light sticky-top flex-md-nowrap ps-6 p-3 search container-fluid">
|
||||
|
||||
{# Mobile Navigation #}
|
||||
<div class="nav-mobile">
|
||||
<div class="nav-mobile-top">
|
||||
<a class="sidebar-logo p-2 d-block" href="{% url 'home' %}">
|
||||
<img src="{% static 'netbox_logo.svg' %}" alt="NetBox logo" width="75%" />
|
||||
</a>
|
||||
<button type="button" aria-label="Toggle Navigation" class="navbar-toggler sidenav-toggle-mobile">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-flex my-1 flex-grow-1 justify-content-center">
|
||||
{% search_options %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-md-flex w-100 search-container">
|
||||
{% search_options %}
|
||||
{% include 'inc/profile_button.html' %}
|
||||
|
||||
{# Desktop Navigation #}
|
||||
<div class="row search-container">
|
||||
|
||||
{# Empty spacer column to ensure search is centered. #}
|
||||
<div class="col-3 d-flex flex-grow-1 ps-0"></div>
|
||||
|
||||
{# Search bar #}
|
||||
<div class="col-6 d-flex flex-grow-1 justify-content-center">
|
||||
{% search_options %}
|
||||
</div>
|
||||
|
||||
{# Proflie/login button #}
|
||||
<div class="col-3 d-flex flex-grow-1 pe-0 justify-content-end">
|
||||
{% include 'inc/profile_button.html' %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
|
||||
{% if settings.BANNER_TOP %}
|
||||
@ -152,12 +100,52 @@
|
||||
{% endif %}
|
||||
|
||||
{# Page footer #}
|
||||
<footer class="footer container-fluid pb-3 pt-4 px-0">
|
||||
<div class="row align-items-center justify-content-end mx-0">
|
||||
<div class="col text-center small text-muted">
|
||||
<footer class="footer container-fluid">
|
||||
<div class="row align-items-center justify-content-between mx-0">
|
||||
|
||||
{# Docs & Community Links #}
|
||||
<div class="col-sm-12 col-md-auto">
|
||||
<nav class="nav justify-content-center justify-content-lg-start">
|
||||
{# Documentation #}
|
||||
<a type="button" class="nav-link" href="{% static 'docs/' %}" target="_blank">
|
||||
<i title="Docs" class="mdi mdi-book-open-variant text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
|
||||
{# REST API #}
|
||||
<a type="button" class="nav-link" href="{% url 'api-root' %}" target="_blank">
|
||||
<i title="REST API" class="mdi mdi-cloud-braces text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
|
||||
{# API docs #}
|
||||
<a type="button" class="nav-link" href="{% url 'api_docs' %}" target="_blank">
|
||||
<i title="REST API documentation" class="mdi mdi-book text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
|
||||
{# GraphQL API #}
|
||||
{% if settings.GRAPHQL_ENABLED %}
|
||||
<a type="button" class="nav-link" href="{% url 'graphql' %}" target="_blank">
|
||||
<i title="GraphQL API" class="mdi mdi-graphql text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{# GitHub #}
|
||||
<a type="button" class="nav-link" href="https://github.com/netbox-community/netbox" target="_blank">
|
||||
<i title="Source Code" class="mdi mdi-github text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
|
||||
{# NetDev Slack #}
|
||||
<a type="button" class="nav-link" href="https://netdev.chat/" target="_blank">
|
||||
<i title="Community" class="mdi mdi-slack text-primary" data-bs-placement="top" data-bs-toggle="tooltip"></i>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{# System Info #}
|
||||
<div class="col-sm-12 col-md-auto text-center text-lg-end small text-muted">
|
||||
<span class="fw-light d-block d-md-inline">{% annotated_now %} {% now 'T' %}</span>
|
||||
<span class="ms-md-3 d-block d-md-inline">{{ settings.HOSTNAME }} (v{{ settings.VERSION }})</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
41
netbox/templates/base/sidenav.html
Normal file
41
netbox/templates/base/sidenav.html
Normal file
@ -0,0 +1,41 @@
|
||||
{% load nav %}
|
||||
{% load static %}
|
||||
|
||||
<nav class="sidenav" id="sidenav" data-simplebar>
|
||||
<div class="sidenav-header">
|
||||
|
||||
{# Brand #}
|
||||
|
||||
{# Full Logo #}
|
||||
<a class="sidenav-brand" href="/">
|
||||
<img src="{% static 'netbox_logo.svg' %}" height="48" class="sidenav-brand-img" alt="NetBox Logo">
|
||||
</a>
|
||||
|
||||
{# Icon Logo #}
|
||||
<a class="sidenav-brand-icon" href="/">
|
||||
<img src="{% static 'netbox_icon.svg' %}" height="48" class="sidenav-brand-img" alt="NetBox Logo">
|
||||
</a>
|
||||
|
||||
{# Pin/Unpin Toggle #}
|
||||
<button class="btn btn-sm btn-ghost-primary sidenav-toggle">
|
||||
<div class="sidenav-toggle-icon">
|
||||
<i class="mdi mdi-pin"></i>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sidenav-inner h-100 mb-auto">
|
||||
|
||||
{# Collapse #}
|
||||
<div class="collapse sidenav-collapse">
|
||||
|
||||
{# Nav Items #}
|
||||
{% nav %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-button-container">
|
||||
{% include 'inc/profile_button.html' %}
|
||||
</div>
|
||||
</nav>
|
@ -95,7 +95,7 @@
|
||||
<td>
|
||||
{% if object.front_image %}
|
||||
<a href="{{ object.front_image.url }}">
|
||||
<img src="{{ object.front_image.url }}" alt="{{ object.front_image.name }}" class="img-responsive" />
|
||||
<img src="{{ object.front_image.url }}" alt="{{ object.front_image.name }}" class="img-fluid" />
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
@ -107,7 +107,7 @@
|
||||
<td>
|
||||
{% if object.rear_image %}
|
||||
<a href="{{ object.rear_image.url }}">
|
||||
<img src="{{ object.rear_image.url }}" alt="{{ object.rear_image.name }}" class="img-responsive" />
|
||||
<img src="{{ object.rear_image.url }}" alt="{{ object.rear_image.name }}" class="img-fluid" />
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
|
@ -8,13 +8,13 @@
|
||||
title="Configure Table"
|
||||
data-bs-target="#{{ table_modal }}"
|
||||
class="btn btn-sm btn-outline-dark"
|
||||
>
|
||||
>
|
||||
<i class="mdi mdi-cog"></i> Configure Table
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col col-md-4 d-flex noprint table-controls">
|
||||
<div class="col col-12 col-lg-4 my-3 my-lg-0 d-flex noprint table-controls">
|
||||
<div class="input-group input-group-sm">
|
||||
<input
|
||||
type="text"
|
||||
|
@ -13,7 +13,7 @@
|
||||
{% for obj_type in results %}
|
||||
<div class="card">
|
||||
<h5 class="card-header">{{ obj_type.name|bettertitle }}</h5>
|
||||
<div class="card-body">
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table obj_type.table 'inc/table.html' %}
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
|
@ -1,79 +1,58 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div id="sidenav-accordion" class="accordion accordion-flush nav-item">
|
||||
{% for menu in nav_items %}
|
||||
<ul class="navbar-nav">
|
||||
{% for menu in nav_items %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#menu{{ menu.label }}" data-bs-toggle="collapse" role="button" aria-expanded="false" aria-controls="menu{{ menu.label }}">
|
||||
<i class="{{ menu.icon_class }}"></i>
|
||||
<span class="nav-link-text">{{ menu.label }}</span>
|
||||
</a>
|
||||
<div class="collapse" id="menu{{ menu.label }}">
|
||||
<ul class="nav nav-sm flex-column">
|
||||
|
||||
{# Main Collapsible Menu #}
|
||||
<div class="accordion-item">
|
||||
<a
|
||||
href="#"
|
||||
role="button"
|
||||
aria-expanded="true"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ menu.label|lower }}"
|
||||
class="d-flex justify-content-between align-items-center accordion-button nav-link collapsed">
|
||||
<span class="fw-bold sidebar-nav-link">
|
||||
<i class="{{ menu.icon_class }} me-1 opacity-50"></i>
|
||||
{{ menu.label }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div id="{{ menu.label|lower }}" class="accordion-collapse collapse" data-bs-parent="#sidenav-accordion">
|
||||
<div class="multi-level accordion-body px-0">
|
||||
|
||||
{% for group in menu.groups %}
|
||||
{# Within each main menu, there are groups of menu items #}
|
||||
<div class="flex-column nav">
|
||||
|
||||
<h6 class="accordion-item-title">{{ group.label }}</h6>
|
||||
|
||||
{% for item in group.items %}
|
||||
{# Each Menu Item #}
|
||||
<div class="nav-item d-flex justify-content-between align-items-center">
|
||||
|
||||
{# Menu Link with Text #}
|
||||
{% if request.user|has_perms:item.permissions %}
|
||||
|
||||
<a class="nav-link flex-grow-1" href="{% url item.link %}">
|
||||
{{ item.link_text }}
|
||||
</a>
|
||||
|
||||
{# Menu item buttons (if any) #}
|
||||
{% if item.buttons %}
|
||||
<div class="btn-group ps-1">
|
||||
{% for button in item.buttons %}
|
||||
{% if request.user|has_perms:button.permissions %}
|
||||
<a class="btn btn-sm btn-{{ button.color }} lh-1" href="{% url button.link %}" title="{{ button.title }}">
|
||||
<i class="{{ button.icon_class }}"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% for group in menu.groups %}
|
||||
{# Within each main menu, there are groups of menu items #}
|
||||
<li class="nav-item">
|
||||
<div class="nav-link">
|
||||
{# Group Label #}
|
||||
<span class="nav-group-label">{{ group.label }}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% for item in group.items %}
|
||||
{# Each Item #}
|
||||
{% if request.user|has_perms:item.permissions %}
|
||||
<li class="nav-item">
|
||||
<a href="{% url item.link %}" class="nav-link">
|
||||
<span class="sidenav-normal">{{ item.link_text }}</span>
|
||||
</a>
|
||||
|
||||
{# Menu item buttons (if any) #}
|
||||
{% if item.buttons %}
|
||||
<div class="btn-group px-2">
|
||||
{% for button in item.buttons %}
|
||||
{% if request.user|has_perms:button.permissions %}
|
||||
<a class="btn btn-sm btn-{{ button.color }} lh-1" href="{% url button.link %}" title="{{ button.title }}">
|
||||
<i class="{{ button.icon_class }}"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% else %}
|
||||
{# Display a disabled link (no permission) #}
|
||||
<li class="nav-item disabled">
|
||||
<a href="#" class="nav-link disabled" aria-disabled="true" disabled>
|
||||
<i class='mdi mdi-lock small'></i>
|
||||
<span class="sidenav-normal">{{ item.link_text }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{# Display a disabled link (no permission) #}
|
||||
<a class="nav-link flex-grow-1 disabled">
|
||||
{{ item.link_text }}
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# Show a divider if not the last group #}
|
||||
{% if forloop.counter != menu.groups|length %}
|
||||
<hr class="dropdown-divider my-2" />
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<form class="input-group w-100" action="{% url 'search' %}" method="get">
|
||||
<form class="input-group" action="{% url 'search' %}" method="get">
|
||||
<input
|
||||
name="q"
|
||||
type="text"
|
||||
@ -7,45 +7,44 @@
|
||||
class="form-control"
|
||||
value="{{ request.GET.q }}"
|
||||
/>
|
||||
|
||||
<input name="obj_type" hidden type="text" class="search-obj-type" />
|
||||
|
||||
<span class="input-group-text search-obj-selected">All Objects</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
data-bs-toggle="dropdown"
|
||||
class="btn btn-outline-secondary dropdown-toggle"
|
||||
>
|
||||
|
||||
<button type="button" aria-expanded="false" data-bs-toggle="dropdown" class="btn btn-outline-secondary dropdown-toggle">
|
||||
<i class="mdi mdi-filter-variant"></i>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-end search-obj-selector">
|
||||
{% for option in options %} {% if option.items|length == 0 %}
|
||||
<li>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
type="button"
|
||||
data-search-value="{{ option.value }}"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><h6 class="dropdown-header">{{ option.label }}</h6></li>
|
||||
{% endif %} {% for item in option.items %}
|
||||
<li>
|
||||
<button
|
||||
class="dropdown-item"
|
||||
type="button"
|
||||
data-search-value="{{ item.value }}"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %} {% if forloop.counter != options|length %}
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
{% endif %} {% endfor %}
|
||||
{% for option in options %}
|
||||
{% if option.items|length == 0 %}
|
||||
<li>
|
||||
<button class="dropdown-item" type="button" data-search-value="{{ option.value }}">
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><h6 class="dropdown-header">{{ option.label }}</h6></li>
|
||||
{% endif %}
|
||||
|
||||
{% for item in option.items %}
|
||||
<li>
|
||||
<button class="dropdown-item" type="button" data-search-value="{{ item.value }}">
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if forloop.counter != options|length %}
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
|
||||
<button class="btn btn-primary" type="submit">
|
||||
<i class="mdi mdi-magnify"></i>
|
||||
</button>
|
||||
|
||||
|
||||
</form>
|
||||
|
Loading…
Reference in New Issue
Block a user