Closes #12128: Refresh the web UI to employ the Tabler CSS framework (#14833)

* Remove dark mode styling

* Condense & rename light mode stylesheet

* Upgrade to Bootstrap 5.3.2

* Swap out Bootstrap for Tabler; remove custom styling

* Update base page layout for Tabler

* Update login page

* Bump node to v18

* Update button styles

* Update object list view

* Tweak navbar size

* Clean up dashboard widgets

* Ditch separate stylesheet for print media

* Remove simplebar

* Remove obsolete sidebar styling

* Clean up object view template

* Clean up object edit template

* Standardize primary button sizing

* Clean up object list styling

* Add buttons for add & import to navigation menu

* Fix global search bar

* Fix slim-select form widget styling

* Fix toast styling

* Set base fonts

* Clean up paginator styling

* Clean up navigation menu group headings

* Clean up footer links

* Clean up card styles

* Move SVG styles to a designated directory

* Restructure SCSS files

* Remove obsolete/redundant dependencies

* Fix icon spacing

* Update background color classes

* Tweak banner & footer styling and spacing

* Fix badge background colors in table content

* Bump @types/bootstrap to 5.2.10

* Clean up form layouts

* Fix object selector button style

* Fix icon padding inside small buttons

* Fix icon & badge spacing inside buttons and tabs

* Hide paginator for empty pages

* Fix hover color for list items (Tabler bug #1694)

* Fix width of checkbox column in empty tables

* Clean up bulk edit template

* Fix border color of reslug button

* Package & serve Google fonts locally

* Fix tab styling

* Reduce vetical space at top of dashboard

* Remove obsolete content-wrapper template block

* Fix icon spacing in dropdown menu items

* Fix color label sizing

* Separate bulk delete form & object list into tabs

* Fix styling of filter group headings

* Fix styling for object changelog & journal views

* Standardize ordering & styling of action buttons

* Fix designation of active menu item

* Automatically expand menu section containing the active link

* Clean up nav menu styling

* Remove button colors; hide buttons except on hover/active

* Highlight menu group containing the active item

* Update & standardize alert styling

* Refactor base templates to ensure consistent display of header content

* Tweak styling for links inside badges

* Clean up top menu

* Fix JSON/YAML toggles for config context data

* Fix object template header

* Constrain tabs to container-xl; tweak header margins

* Fix object identifier styling

* Fix positioning of card header buttons

* Remove padding from HTMX tables inside cards

* Ensure consistent use of row headings in attribute tables

* Remove padding surrounding tables inside cards

* Remove obsolete CSS classes

* Misc cleanup of old styling

* Refactor 'controls' template block; ditch old classes

* Fix login button sizing

* Limit object edit form width

* Append asterisk to required form field labels

* Remove obsolete styling

* Remove obsolete styling

* Fix position of progress bar outside label

* Fix alignment of delete button in report/script lists

* Fix <pre> styling

* Clean up page headers

* Replace SVG icons with Material Design icons

* Restore dark mode togle functionality

* Fix top navbar background color under dark mode

* Rebuild static assets
This commit is contained in:
Jeremy Stretch 2024-01-17 16:25:42 -05:00 committed by GitHub
parent 8254e707b6
commit 073c2dc8ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
384 changed files with 11558 additions and 9996 deletions

View File

@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
node-version: ['14.x']
node-version: ['18.x']
services:
redis:
image: redis

View File

@ -21,37 +21,33 @@ WEIGHT = """
"""
DEVICE_LINK = """
{{ value|default:'<span class="badge bg-info">Unnamed device</span>' }}
{{ value|default:'<span class="badge text-bg-info">Unnamed device</span>' }}
"""
DEVICEBAY_STATUS = """
{% if record.installed_device_id %}
<span class="badge bg-{{ record.installed_device.get_status_color }}">
<span class="badge text-bg-{{ record.installed_device.get_status_color }}">
{{ record.installed_device.get_status_display }}
</span>
{% else %}
<span class="badge bg-secondary">Vacant</span>
<span class="badge text-bg-secondary">Vacant</span>
{% endif %}
"""
INTERFACE_IPADDRESSES = """
<div class="table-badge-group">
{% for ip in value.all %}
{% if ip.status != 'active' %}
<a href="{{ ip.get_absolute_url }}" class="table-badge badge bg-{{ ip.get_status_color }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
<a href="{{ ip.get_absolute_url }}" class="badge text-bg-{{ ip.get_status_color }}" data-bs-toggle="tooltip" data-bs-placement="left" title="{{ ip.get_status_display }}">{{ ip }}</a>
{% else %}
<a href="{{ ip.get_absolute_url }}" class="table-badge">{{ ip }}</a>
<a href="{{ ip.get_absolute_url }}">{{ ip }}</a>
{% endif %}
{% endfor %}
</div>
"""
INTERFACE_FHRPGROUPS = """
<div class="table-badge-group">
{% for assignment in value.all %}
<a href="{{ assignment.group.get_absolute_url }}">{{ assignment.group.get_protocol_display }}: {{ assignment.group.group_id }}</a>
{% endfor %}
</div>
"""
INTERFACE_TAGGED_VLANS = """

View File

@ -3353,6 +3353,7 @@ class VirtualChassisEditView(ObjectPermissionRequiredMixin, GetReturnURLMixin, V
formset = VCMemberFormSet(queryset=members_queryset)
return render(request, 'dcim/virtualchassis_edit.html', {
'object': virtual_chassis,
'vc_form': vc_form,
'formset': formset,
'return_url': self.get_return_url(request, virtual_chassis),

View File

@ -18,7 +18,7 @@ __all__ = (
'RoleTable',
)
AVAILABLE_LABEL = mark_safe('<span class="badge bg-success">Available</span>')
AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span>')
AGGREGATE_COPY_BUTTON = """
{% copy_content record.pk prefix="aggregate_" %}

View File

@ -18,7 +18,7 @@ __all__ = (
'VLANVirtualMachinesTable',
)
AVAILABLE_LABEL = mark_safe('<span class="badge bg-success">Available</span>')
AVAILABLE_LABEL = mark_safe('<span class="badge text-bg-success">Available</span>')
VLAN_LINK = """
{% if record.pk %}

View File

@ -79,8 +79,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')):
link=f'{app_label}:{model_name}_add',
title='Add',
icon_class='mdi mdi-plus-thick',
permissions=[f'{app_label}.add_{model_name}'],
color=ButtonColorChoices.GREEN
permissions=[f'{app_label}.add_{model_name}']
)
)
if 'import' in actions:
@ -89,8 +88,7 @@ def get_model_buttons(app_label, model_name, actions=('add', 'import')):
link=f'{app_label}:{model_name}_import',
title='Import',
icon_class='mdi mdi-upload',
permissions=[f'{app_label}.add_{model_name}'],
color=ButtonColorChoices.CYAN
permissions=[f'{app_label}.add_{model_name}']
)
)

View File

@ -386,15 +386,13 @@ ADMIN_MENU = Menu(
link=f'users:netboxuser_add',
title='Add',
icon_class='mdi mdi-plus-thick',
permissions=[f'auth.add_user'],
color=ButtonColorChoices.GREEN
permissions=[f'auth.add_user']
),
MenuItemButton(
link=f'users:netboxuser_import',
title='Import',
icon_class='mdi mdi-upload',
permissions=[f'auth.add_user'],
color=ButtonColorChoices.CYAN
permissions=[f'auth.add_user']
)
)
),
@ -409,15 +407,13 @@ ADMIN_MENU = Menu(
link=f'users:netboxgroup_add',
title='Add',
icon_class='mdi mdi-plus-thick',
permissions=[f'auth.add_group'],
color=ButtonColorChoices.GREEN
permissions=[f'auth.add_group']
),
MenuItemButton(
link=f'users:netboxgroup_import',
title='Import',
icon_class='mdi mdi-upload',
permissions=[f'auth.add_group'],
color=ButtonColorChoices.CYAN
permissions=[f'auth.add_group']
)
)
),

View File

@ -161,8 +161,11 @@ class ToggleColumn(tables.CheckBoxColumn):
visible = kwargs.pop('visible', False)
if 'attrs' not in kwargs:
kwargs['attrs'] = {
'th': {
'class': 'w-1',
},
'td': {
'class': 'min-width',
'class': 'w-1',
},
'input': {
'class': 'form-check-input'
@ -322,7 +325,7 @@ class ChoiceFieldColumn(tables.Column):
except AttributeError:
bg_color = self.DEFAULT_BG_COLOR
return mark_safe(f'<span class="badge bg-{bg_color}">{value}</span>')
return mark_safe(f'<span class="badge text-bg-{bg_color}">{value}</span>')
def value(self, value):
return value

View File

@ -1,6 +1,6 @@
SEARCH_RESULT_ATTRS = """
{% for name, value in record.display_attrs.items %}
<span class="badge bg-secondary"
<span class="badge text-bg-secondary"
{% if value|length > 40 %} data-bs-toggle="tooltip" data-bs-placement="bottom" title="{{ value }}"{% endif %}
>
{{ name|bettertitle }}:

View File

@ -73,12 +73,10 @@ async function bundleScripts() {
async function bundleStyles() {
try {
const entryPoints = {
'netbox-external': 'styles/_external.scss',
'netbox-light': 'styles/_light.scss',
'netbox-dark': 'styles/_dark.scss',
'netbox-print': 'styles/_print.scss',
rack_elevation: 'styles/_rack_elevation.scss',
cable_trace: 'styles/_cable_trace.scss',
'netbox-external': 'styles/external.scss',
'netbox': 'styles/netbox.scss',
rack_elevation: 'styles/svg/rack_elevation.scss',
cable_trace: 'styles/svg/cable_trace.scss',
graphiql: 'netbox-graphiql/graphiql.scss',
};
const pluginOptions = { outputStyle: 'compressed' };
@ -102,8 +100,7 @@ async function bundleStyles() {
});
if (result.errors.length === 0) {
for (const [targetName, sourceName] of Object.entries(entryPoints)) {
const source = sourceName.split('/')[1];
console.log(`✅ Bundled source file '${source}' to '${targetName}.css'`);
console.log(`✅ Bundled source file '${sourceName}' to '${targetName}.css'`);
}
}
} catch (err) {

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.

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.

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.

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.

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.

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.

BIN
netbox/project-static/dist/netbox.css vendored Normal file

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.

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

4108
netbox/project-static/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -23,8 +23,7 @@
},
"dependencies": {
"@mdi/font": "^7.0.96",
"@popperjs/core": "^2.11.6",
"bootstrap": "~5.0.2",
"@tabler/core": "1.0.0-beta20",
"clipboard": "^2.0.11",
"color2k": "^2.0.0",
"dayjs": "^1.11.5",
@ -35,13 +34,13 @@
"just-debounce-it": "^3.1.1",
"query-string": "^7.1.1",
"sass": "^1.55.0",
"simplebar": "^5.3.9",
"slim-select": "^1.27.1"
"slim-select": "^1.27.1",
"typeface-inter": "^3.18.1",
"typeface-roboto-mono": "^1.1.13"
},
"devDependencies": {
"@types/bootstrap": "^5.0.17",
"@types/bootstrap": "5.2.10",
"@types/cookie": "^0.5.1",
"@types/masonry-layout": "^4.2.5",
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"esbuild": "^0.13.15",

View File

@ -1,10 +1,6 @@
import { getElements, isTruthy } from './util';
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';
/**
* Determine if a value is a supported color mode string value.
@ -24,23 +20,11 @@ function storeColorMode(mode: ColorMode): void {
}
function updateElements(targetMode: ColorMode): void {
document.documentElement.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);
}
const body = document.querySelector('body');
if (body && targetMode == 'dark') {
body.setAttribute('data-bs-theme', 'dark');
} else if (body) {
body.setAttribute('data-bs-theme', 'light');
}
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
@ -57,9 +41,8 @@ function updateElements(targetMode: ColorMode): void {
* @param mode Target color mode.
*/
export function setColorMode(mode: ColorMode): void {
for (const func of [storeColorMode, updateElements]) {
func(mode);
}
storeColorMode(mode);
updateElements(mode);
}
/**

View File

@ -1,5 +1,4 @@
import '@popperjs/core';
import 'bootstrap';
import 'htmx.org';
import 'simplebar';
import './netbox';

View File

@ -5,7 +5,7 @@ import { Toast } from 'bootstrap';
*/
export function initMessages(): void {
const elements = document.querySelectorAll<HTMLDivElement>(
'body > div#django-messages > div.django-message.toast',
'body > div#django-messages > div.toast',
);
for (const element of elements) {
if (element !== null) {

View File

@ -204,23 +204,25 @@ class SideNav {
* @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');
private activateLink(link: HTMLDivElement, action: 'expand' | 'collapse'): void {
// Find the closest .dropdown-menu element, which should contain `link`.
const dropdownMenu = link.closest('.dropdown-menu') as Nullable<HTMLDivElement>;
if (isElement(dropdownMenu)) {
// Find the closest `.nav-link`, which should be adjacent to the `.dropdown-menu` element.
const groupItem = dropdownMenu.parentElement;
const groupLink = dropdownMenu.parentElement?.querySelector('.nav-link');
if (isElement(groupLink) && isElement(groupItem)) {
switch (action) {
case 'expand':
groupLink.setAttribute('aria-expanded', 'true');
collapse.classList.add('show');
groupItem.classList.add('active');
dropdownMenu.classList.add('show');
link.classList.add('active');
break;
case 'collapse':
groupLink.setAttribute('aria-expanded', 'false');
collapse.classList.remove('show');
groupItem.classList.remove('active');
dropdownMenu.classList.remove('show');
link.classList.remove('active');
break;
}
@ -232,13 +234,16 @@ class SideNav {
* 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',
private *getActiveLinks(): Generator<HTMLDivElement> {
for (const menuitem of this.base.querySelectorAll<HTMLDivElement>(
'ul.navbar-nav .nav-item .dropdown-item',
)) {
const href = new RegExp(link.href, 'gi');
if (window.location.href.match(href)) {
yield link;
const link = menuitem.querySelector<HTMLAnchorElement>('a')
if (link) {
const href = new RegExp(link.href, 'gi');
if (window.location.href.match(href)) {
yield menuitem;
}
}
}
}
@ -309,7 +314,7 @@ class SideNav {
}
export function initSideNav(): void {
for (const sidenav of getElements<HTMLDivElement>('.sidenav')) {
for (const sidenav of getElements<HTMLDivElement>('.navbar')) {
new SideNav(sidenav);
}
}

View File

@ -1,2 +0,0 @@
@import './theme-light.scss';
@import './cable-trace.scss';

View File

@ -1,10 +0,0 @@
// Entry for netbox-dark.css stylesheet.
html[data-netbox-color-mode='dark'] {
// Imports are scoped under the body when its data-netbox-color-mode attribute is set to 'dark'.
@import './theme-dark.scss';
@import './bootstrap.scss';
@import './select.scss';
@import './flatpickr-dark.scss';
@import './netbox.scss';
}

View File

@ -1,5 +0,0 @@
// Entry for all 3rd party library imports that do not rely on Bootstrap or NetBox styles.
@import '../node_modules/@mdi/font/css/materialdesignicons.min.css';
@import '../node_modules/flatpickr/dist/flatpickr.css';
@import '../node_modules/simplebar/dist/simplebar.css';
@import 'gridstack/dist/gridstack.min.css';

View File

@ -1,6 +0,0 @@
// Entry for netbox-light.css stylesheet.
@import './theme-light.scss';
@import './bootstrap.scss';
@import './select.scss';
@import './netbox.scss';

View File

@ -1,18 +0,0 @@
// Entry for netbox-print.css. Force light-mode theming when printing.
@media print {
// Force black and white background/foreground colors when printing.
:root {
--nbx-body-bg: #fff !important;
--nbx-body-color: #000 !important;
}
html,
html[data-netbox-color-mode='dark'],
html[data-netbox-color-mode='light'] {
@import './theme-light';
@import './bootstrap';
@import './select';
@import './netbox';
}
}

View File

@ -1,2 +0,0 @@
@import './theme-light.scss';
@import './rack-elevation.scss';

View File

@ -0,0 +1,20 @@
// Global variables
// Set base fonts
$font-family-base: 'Inter';
// See https://github.com/tabler/tabler/issues/1812
$font-family-monospace: 'Roboto Mono';
// Set the navigation sidebar width
$sidebar-width: 18rem;
// Reduce the default button padding
$btn-padding-x: 0.5rem;
$btn-padding-y: 0.25rem;
// Reduce the default table cell padding
$table-cell-padding-x: 0.5rem;
$table-cell-padding-y: 0.5rem;
// Fix Tabler bug #1694 in 1.0.0-beta20
$hover-bg: rgba(var(--tblr-secondary-rgb), 0.08);

View File

@ -1,35 +0,0 @@
// Import the rest of bootstrap.
@import '../node_modules/bootstrap/scss/utilities';
@import './extensions';
@import '../node_modules/bootstrap/scss/mixins';
@import '../node_modules/bootstrap/scss/root';
@import '../node_modules/bootstrap/scss/reboot';
@import '../node_modules/bootstrap/scss/type';
@import '../node_modules/bootstrap/scss/images';
@import '../node_modules/bootstrap/scss/containers';
@import '../node_modules/bootstrap/scss/grid';
@import '../node_modules/bootstrap/scss/tables';
@import '../node_modules/bootstrap/scss/forms';
@import '../node_modules/bootstrap/scss/buttons';
@import '../node_modules/bootstrap/scss/transitions';
@import '../node_modules/bootstrap/scss/dropdown';
@import '../node_modules/bootstrap/scss/button-group';
@import '../node_modules/bootstrap/scss/nav';
@import '../node_modules/bootstrap/scss/navbar';
@import '../node_modules/bootstrap/scss/card';
@import '../node_modules/bootstrap/scss/accordion';
@import '../node_modules/bootstrap/scss/breadcrumb';
@import '../node_modules/bootstrap/scss/pagination';
@import '../node_modules/bootstrap/scss/badge';
@import '../node_modules/bootstrap/scss/alert';
@import '../node_modules/bootstrap/scss/progress';
@import '../node_modules/bootstrap/scss/list-group';
@import '../node_modules/bootstrap/scss/close';
@import '../node_modules/bootstrap/scss/toasts';
@import '../node_modules/bootstrap/scss/modal';
@import '../node_modules/bootstrap/scss/tooltip';
@import '../node_modules/bootstrap/scss/popover';
@import '../node_modules/bootstrap/scss/carousel';
@import '../node_modules/bootstrap/scss/spinners';
@import '../node_modules/bootstrap/scss/helpers';
@import '../node_modules/bootstrap/scss/utilities/api';

View File

@ -0,0 +1,40 @@
// Serialized data from change records
pre.change-data {
padding-right: 0;
padding-left: 0;
// Display each line individually for highlighting
> span {
display: block;
padding-right: $spacer;
padding-left: $spacer;
&.added {
background-color: $green;
}
&.removed {
background-color: $red;
}
}
}
// Change data diff w/added & removed data
pre.change-diff {
border-color: transparent;
&.change-added {
background-color: $green;
}
&.change-removed {
background-color: $red;
}
}
// <pre> elements displayed with a border
pre.block {
padding: $spacer;
border: 1px solid $border-color;
border-radius: $border-radius;
}

View File

@ -0,0 +1,60 @@
@use 'sass:map';
// Color labels
span.color-label {
display: block;
width: 5rem;
height: 1rem;
padding: $badge-padding-y $badge-padding-x;
border: 1px solid #303030;
border-radius: $border-radius;
}
// Rendered Markdown
.rendered-markdown {
table {
width: 100%;
// Apply a border
th {
border-bottom: 2px solid #dddddd;
padding: 8px;
}
td {
border-top: 1px solid #dddddd;
padding: 8px;
}
// Map "align" attr of column headings
th[align="left"] {
text-align: left;
}
th[align="center"] {
text-align: center;
}
th[align="right"] {
text-align: right;
}
}
}
// Object hierarchy depth indicators
.record-depth {
display: inline;
user-select: none;
opacity: 33%;
// Add spacing to the last or only dot.
span:only-of-type,
span:last-of-type {
margin-right: map.get($spacers, 1);
}
}
// Hides the last child of an element
.hide-last-child :last-child {
visibility: hidden;
opacity: 0;
}

Some files were not shown because too many files have changed in this diff Show More