nautobot 3.0.0rc1__py3-none-any.whl → 3.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/apps/forms.py +8 -0
- nautobot/apps/templatetags.py +231 -0
- nautobot/apps/testing.py +11 -1
- nautobot/apps/ui.py +21 -1
- nautobot/apps/utils.py +26 -1
- nautobot/core/celery/__init__.py +46 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +185 -44
- nautobot/core/cli/bootstrap_v3_to_v5_changes.yaml +314 -0
- nautobot/core/graphql/generators.py +2 -2
- nautobot/core/jobs/bulk_actions.py +12 -6
- nautobot/core/jobs/cleanup.py +13 -1
- nautobot/core/settings.py +13 -0
- nautobot/core/settings.yaml +22 -0
- nautobot/core/settings_funcs.py +11 -1
- nautobot/core/tables.py +19 -1
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/header_extra_content_table.html +9 -3
- nautobot/core/templates/generic/object_create.html +1 -1
- nautobot/core/templates/inc/header.html +9 -10
- nautobot/core/templates/login.html +16 -1
- nautobot/core/templates/nautobot_config.py.j2 +14 -1
- nautobot/core/templates/redoc_ui.html +3 -0
- nautobot/core/templatetags/helpers.py +3 -3
- nautobot/core/testing/views.py +3 -1
- nautobot/core/tests/test_graphql.py +13 -0
- nautobot/core/tests/test_jobs.py +118 -0
- nautobot/core/tests/test_views.py +24 -0
- nautobot/core/ui/bulk_buttons.py +2 -3
- nautobot/core/utils/lookup.py +2 -3
- nautobot/core/utils/permissions.py +1 -1
- nautobot/core/views/generic.py +1 -0
- nautobot/core/views/mixins.py +37 -10
- nautobot/core/views/renderers.py +1 -0
- nautobot/core/views/utils.py +3 -3
- nautobot/data_validation/views.py +1 -9
- nautobot/dcim/forms.py +9 -9
- nautobot/dcim/models/devices.py +3 -3
- nautobot/dcim/tables/power.py +3 -0
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +1 -1
- nautobot/dcim/views.py +30 -44
- nautobot/extras/api/views.py +14 -3
- nautobot/extras/choices.py +3 -0
- nautobot/extras/jobs.py +48 -2
- nautobot/extras/migrations/0132_approval_workflow_seed_data.py +127 -0
- nautobot/extras/models/approvals.py +11 -1
- nautobot/extras/models/models.py +19 -0
- nautobot/extras/models/relationships.py +3 -1
- nautobot/extras/tables.py +35 -18
- nautobot/extras/templates/extras/approval_workflow/approve.html +9 -2
- nautobot/extras/templates/extras/approval_workflow/deny.html +9 -3
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +1 -1
- nautobot/extras/templates/extras/customfield_update.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_update.html +2 -2
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +10 -2
- nautobot/extras/templates/extras/inc/job_tiles.html +2 -2
- nautobot/extras/templates/extras/inc/jobresult.html +1 -1
- nautobot/extras/templates/extras/metadatatype_create.html +1 -1
- nautobot/extras/templates/extras/object_approvalworkflow.html +2 -3
- nautobot/extras/templates/extras/secretsgroup_update.html +1 -1
- nautobot/extras/tests/test_api.py +57 -3
- nautobot/extras/tests/test_customfields_filters.py +84 -4
- nautobot/extras/tests/test_views.py +323 -6
- nautobot/extras/views.py +114 -39
- nautobot/ipam/constants.py +2 -2
- nautobot/ipam/tables.py +7 -6
- nautobot/load_balancers/constants.py +6 -0
- nautobot/load_balancers/migrations/0001_initial.py +14 -3
- nautobot/load_balancers/models.py +5 -4
- nautobot/load_balancers/tables.py +5 -0
- nautobot/project-static/dist/css/nautobot.css +1 -1
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/graphql-libraries.js +1 -1
- nautobot/project-static/dist/js/graphql-libraries.js.map +1 -1
- nautobot/project-static/dist/js/libraries.js +1 -1
- nautobot/project-static/dist/js/libraries.js.LICENSE.txt +38 -2
- nautobot/project-static/dist/js/libraries.js.map +1 -1
- nautobot/project-static/dist/js/nautobot-graphiql.js +1 -1
- nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -1
- nautobot/project-static/dist/js/nautobot.js +1 -1
- nautobot/project-static/dist/js/nautobot.js.map +1 -1
- nautobot/project-static/img/dark-theme.png +0 -0
- nautobot/project-static/img/light-theme.png +0 -0
- nautobot/project-static/img/system-theme.png +0 -0
- nautobot/project-static/js/forms.js +1 -85
- nautobot/tenancy/tables.py +3 -2
- nautobot/tenancy/views.py +3 -2
- nautobot/ui/package-lock.json +553 -569
- nautobot/ui/package.json +10 -10
- nautobot/ui/src/js/checkbox.js +132 -0
- nautobot/ui/src/js/nautobot.js +6 -0
- nautobot/ui/src/js/select2.js +69 -73
- nautobot/ui/src/js/theme.js +129 -39
- nautobot/ui/src/scss/nautobot.scss +11 -1
- nautobot/vpn/templates/vpn/vpnprofile_create.html +2 -2
- nautobot/wireless/filters.py +15 -1
- nautobot/wireless/tables.py +18 -14
- nautobot/wireless/templates/wireless/wirelessnetwork_create.html +1 -1
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/METADATA +2 -2
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/RECORD +103 -98
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/WHEEL +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/entry_points.txt +0 -0
nautobot/ui/package.json
CHANGED
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@graphiql/plugin-explorer": "0.1.15",
|
|
16
|
-
"@graphiql/toolkit": "^0.11.
|
|
16
|
+
"@graphiql/toolkit": "^0.11.3",
|
|
17
17
|
"@mdi/font": "^7.4.47",
|
|
18
18
|
"@popperjs/core": "^2.11.8",
|
|
19
|
-
"bootstrap": "^5.3.
|
|
20
|
-
"clipboard": "2.0.
|
|
19
|
+
"bootstrap": "^5.3.8",
|
|
20
|
+
"clipboard": "2.0.11",
|
|
21
21
|
"echarts": "^6.0.0",
|
|
22
|
-
"flatpickr": "4.6.
|
|
22
|
+
"flatpickr": "4.6.13",
|
|
23
23
|
"graphiql": "2.4.7",
|
|
24
24
|
"graphql": "16.10.0",
|
|
25
25
|
"graphql-ws": "5.13.1",
|
|
26
26
|
"highlight.js": "^11.11.1",
|
|
27
|
-
"htmx.org": "^2.0.
|
|
27
|
+
"htmx.org": "^2.0.8",
|
|
28
28
|
"jquery": "^3.7.1",
|
|
29
29
|
"jquery-ui": "^1.14.1",
|
|
30
30
|
"lodash.get": "^4.4.2",
|
|
@@ -33,23 +33,23 @@
|
|
|
33
33
|
"react-dom": "17.0.2",
|
|
34
34
|
"select2": "4.0.13",
|
|
35
35
|
"select2-bootstrap-5-theme": "^1.3.0",
|
|
36
|
-
"whatwg-fetch": "3.6.
|
|
36
|
+
"whatwg-fetch": "3.6.20"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@eslint/js": "^9.32.0",
|
|
40
|
-
"autoprefixer": "^10.4.
|
|
40
|
+
"autoprefixer": "^10.4.22",
|
|
41
41
|
"copy-webpack-plugin": "^13.0.1",
|
|
42
42
|
"css-loader": "^7.1.2",
|
|
43
43
|
"eslint": "^9.32.0",
|
|
44
44
|
"eslint-plugin-import": "^2.32.0",
|
|
45
45
|
"globals": "^16.3.0",
|
|
46
|
-
"mini-css-extract-plugin": "^2.9.
|
|
46
|
+
"mini-css-extract-plugin": "^2.9.4",
|
|
47
47
|
"npm-run-all2": "^8.0.4",
|
|
48
|
-
"postcss": "^8.5.
|
|
48
|
+
"postcss": "^8.5.6",
|
|
49
49
|
"postcss-loader": "^8.1.1",
|
|
50
50
|
"prettier": "^3.5.3",
|
|
51
51
|
"sass": "^1.84.0",
|
|
52
|
-
"sass-loader": "^16.0.
|
|
52
|
+
"sass-loader": "^16.0.6",
|
|
53
53
|
"webpack": "^5.98.0",
|
|
54
54
|
"webpack-cli": "^6.0.1"
|
|
55
55
|
},
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const ITEM_CHECKBOX_SELECTOR = 'input[type="checkbox"][name="pk"]';
|
|
2
|
+
const TOGGLE_CHECKBOX_SELECTOR = 'input[type="checkbox"].toggle';
|
|
3
|
+
const SELECT_ALL_BOX_SELECTOR = '#select_all_box';
|
|
4
|
+
const SELECT_ALL_CHECKBOX_SELECTOR = '#select_all';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Set checkbox `checked` state while dispatching proper events. Do nothing if `checkbox.checked` state is already the
|
|
8
|
+
* same as passed `checked` parameter.
|
|
9
|
+
* @param {HTMLInputElement} checkbox - Checkbox HTML element.
|
|
10
|
+
* @param {boolean} checked - `checked` state to be set.
|
|
11
|
+
* @returns {void} Do not return any value, set given HTML element state and dispatch `'change'` and `'input'` events.
|
|
12
|
+
*/
|
|
13
|
+
const setChecked = (checkbox, checked) => {
|
|
14
|
+
if (checkbox.checked !== checked) {
|
|
15
|
+
checkbox.checked = checked;
|
|
16
|
+
// Defer dispatching events with `setTimeout` to prevent handling intermediate states during sequential updates.
|
|
17
|
+
setTimeout(() => {
|
|
18
|
+
checkbox.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
|
|
19
|
+
checkbox.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize custom logic handlers for `class="toggle ..."`, `name="pk"` and `id="select-all" checkboxes.
|
|
26
|
+
* @returns {void} Do not return any value, just initialize proper custom logic handlers for specific checkboxes.
|
|
27
|
+
*/
|
|
28
|
+
export const initializeCheckboxes = () => {
|
|
29
|
+
// Track the last selected checkbox index for range selection.
|
|
30
|
+
let lastSelectedIndex = null;
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* `onClick` and `onInput` event handlers both act on the same checkbox elements, but in slightly different scenarios:
|
|
34
|
+
* 1. `onClick` is specifically tied to manual user interaction and is called only when user directly interacts with
|
|
35
|
+
* checkbox element. It handles:
|
|
36
|
+
* 1.1. Checking/unchecking all individual checkboxes in the table when "toggle" checkbox is clicked.
|
|
37
|
+
* 1.2. Shift-click range selection for individual checkboxes in the table.
|
|
38
|
+
* 2. `onInput` is called after checkbox state is changed, regardless of the change being triggered programmatically
|
|
39
|
+
* or by manual user interaction. It handles:
|
|
40
|
+
* 2.1. Showing/hiding "select all" box when "toggle" checkbox is checked/unchecked.
|
|
41
|
+
* 2.2. Checking/unchecking "toggle" checkbox based on the collective state of individual checkboxes.
|
|
42
|
+
* 2.3. Enabling/disabling buttons in "select all" box based on its checkbox state.
|
|
43
|
+
*/
|
|
44
|
+
const onClick = (event) => {
|
|
45
|
+
// "Toggle" checkbox for object lists (PK column). Notice distinction between handling `'click'` and `'input'` events.
|
|
46
|
+
const toggleCheckbox = event.target.closest(TOGGLE_CHECKBOX_SELECTOR);
|
|
47
|
+
if (toggleCheckbox) {
|
|
48
|
+
const isChecked = toggleCheckbox.checked;
|
|
49
|
+
|
|
50
|
+
// Check/uncheck all PK column checkboxes in the table.
|
|
51
|
+
toggleCheckbox
|
|
52
|
+
.closest('table')
|
|
53
|
+
.querySelectorAll(`${ITEM_CHECKBOX_SELECTOR}:not(.visually-hidden)`)
|
|
54
|
+
.forEach((checkbox) => setChecked(checkbox, isChecked));
|
|
55
|
+
|
|
56
|
+
// Reset last selected index when using toggle all
|
|
57
|
+
lastSelectedIndex = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Individual row item checkbox in object lists (PK column). Notice distinction between handling `'click'` and `'input'` events.
|
|
61
|
+
const itemCheckbox = event.target.closest(ITEM_CHECKBOX_SELECTOR);
|
|
62
|
+
if (itemCheckbox) {
|
|
63
|
+
const table = itemCheckbox.closest('table');
|
|
64
|
+
const allCheckboxes = [...table.querySelectorAll(`${ITEM_CHECKBOX_SELECTOR}:not(.visually-hidden)`)];
|
|
65
|
+
const currentIndex = allCheckboxes.indexOf(itemCheckbox);
|
|
66
|
+
|
|
67
|
+
// Handle shift-click for range selection/deselection in PK column.
|
|
68
|
+
if (event.shiftKey && lastSelectedIndex !== null) {
|
|
69
|
+
// Create range from previous click to current click
|
|
70
|
+
const startIndex = Math.min(lastSelectedIndex, currentIndex);
|
|
71
|
+
const endIndex = Math.max(lastSelectedIndex, currentIndex);
|
|
72
|
+
|
|
73
|
+
// Use the clicked item's new state for entire range
|
|
74
|
+
const shouldSelect = itemCheckbox.checked;
|
|
75
|
+
|
|
76
|
+
// Apply to entire range
|
|
77
|
+
allCheckboxes.slice(startIndex, endIndex + 1).forEach((checkbox) => setChecked(checkbox, shouldSelect));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Always update anchor to current click (normal click or shift+click)
|
|
81
|
+
lastSelectedIndex = currentIndex;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const onInput = (event) => {
|
|
86
|
+
// "Toggle" checkbox for object lists (PK column). Notice distinction between handling `'click'` and `'input'` events.
|
|
87
|
+
const toggleCheckbox = event.target.closest(TOGGLE_CHECKBOX_SELECTOR);
|
|
88
|
+
if (toggleCheckbox) {
|
|
89
|
+
const isChecked = toggleCheckbox.checked;
|
|
90
|
+
|
|
91
|
+
// Show/hide the select all objects form that contains the bulk action buttons.
|
|
92
|
+
const selectAllBox = document.querySelector(SELECT_ALL_BOX_SELECTOR);
|
|
93
|
+
selectAllBox?.classList.toggle('visually-hidden', !isChecked);
|
|
94
|
+
|
|
95
|
+
if (selectAllBox && !isChecked) {
|
|
96
|
+
const selectAll = document.querySelector(SELECT_ALL_CHECKBOX_SELECTOR);
|
|
97
|
+
if (selectAll) {
|
|
98
|
+
setChecked(selectAll, false);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Individual row item checkbox in object lists (PK column). Notice distinction between handling `'click'` and `'input'` events.
|
|
104
|
+
const itemCheckbox = event.target.closest(ITEM_CHECKBOX_SELECTOR);
|
|
105
|
+
if (itemCheckbox) {
|
|
106
|
+
const table = itemCheckbox.closest('table');
|
|
107
|
+
const allCheckboxes = [...table.querySelectorAll(`${ITEM_CHECKBOX_SELECTOR}:not(.visually-hidden)`)];
|
|
108
|
+
|
|
109
|
+
// Check or uncheck the "toggle" checkbox if all items are checked or any item is unchecked, respectively.
|
|
110
|
+
const tableToggleCheckbox = table.querySelector(TOGGLE_CHECKBOX_SELECTOR);
|
|
111
|
+
if (tableToggleCheckbox) {
|
|
112
|
+
const hasUnchecked = allCheckboxes.some((checkbox) => !checkbox.checked);
|
|
113
|
+
setChecked(tableToggleCheckbox, !hasUnchecked);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// `selectAll` is the checkbox that selects all objects.
|
|
118
|
+
const selectAll = event.target.closest(SELECT_ALL_CHECKBOX_SELECTOR);
|
|
119
|
+
// `selectAllBox` is the form bulk action buttons container.
|
|
120
|
+
const selectAllBox = event.target.closest(SELECT_ALL_BOX_SELECTOR);
|
|
121
|
+
if (selectAll && selectAllBox) {
|
|
122
|
+
// If the `selectAll` checkbox is checked, enable all form bulk action buttons.
|
|
123
|
+
const isChecked = selectAll.checked;
|
|
124
|
+
selectAllBox.querySelectorAll('button').forEach((button) => {
|
|
125
|
+
button.disabled = !isChecked;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
document.addEventListener('click', onClick);
|
|
131
|
+
document.addEventListener('input', onInput);
|
|
132
|
+
};
|
nautobot/ui/src/js/nautobot.js
CHANGED
|
@@ -34,8 +34,10 @@ window.jQuery = jQuery;
|
|
|
34
34
|
window.$ = window.jQuery;
|
|
35
35
|
|
|
36
36
|
import 'jquery-ui';
|
|
37
|
+
import 'jquery-ui/ui/widgets/sortable.js';
|
|
37
38
|
import 'select2';
|
|
38
39
|
|
|
40
|
+
import { initializeCheckboxes } from './checkbox.js';
|
|
39
41
|
import { initializeCollapseToggleAll } from './collapse.js';
|
|
40
42
|
import { initializeDraggable } from './draggable.js';
|
|
41
43
|
import { initializeDrawers } from './drawer.js';
|
|
@@ -53,6 +55,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
53
55
|
// History
|
|
54
56
|
loadState();
|
|
55
57
|
window.nb.history = { saveState };
|
|
58
|
+
|
|
56
59
|
// Tooltips
|
|
57
60
|
// https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips
|
|
58
61
|
[...document.querySelectorAll('[data-bs-toggle="tooltip"]')].forEach((tooltip) => new bootstrap.Tooltip(tooltip));
|
|
@@ -60,6 +63,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
60
63
|
// Sidenav
|
|
61
64
|
initializeSidenav();
|
|
62
65
|
|
|
66
|
+
// Checkbox
|
|
67
|
+
window.nb.checkbox = { initializeCheckboxes };
|
|
68
|
+
|
|
63
69
|
// Collapse
|
|
64
70
|
initializeCollapseToggleAll();
|
|
65
71
|
|
nautobot/ui/src/js/select2.js
CHANGED
|
@@ -134,80 +134,76 @@ const initializeDynamicChoiceSelection = (context, dropdownParent = null) => {
|
|
|
134
134
|
const content_type = element.getAttribute('data-contenttype');
|
|
135
135
|
|
|
136
136
|
// Attach any extra query parameters
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
137
|
+
const extra_query_parameters_array = [...element.attributes]
|
|
138
|
+
.filter((attribute) => attribute.name.includes('data-query-param-'))
|
|
139
|
+
.flatMap((attribute) => {
|
|
140
|
+
const [, param_name] = attribute.name.split('data-query-param-');
|
|
141
|
+
|
|
142
|
+
const values = (() => {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(attribute.value);
|
|
145
|
+
// eslint-disable-next-line no-unused-vars
|
|
146
|
+
} catch (exception) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
|
|
151
|
+
return values.flatMap((value) => {
|
|
152
|
+
const has_ref_field = value.startsWith('$');
|
|
153
|
+
|
|
154
|
+
// Referencing the value of another form field.
|
|
155
|
+
const ref_field = has_ref_field
|
|
156
|
+
? (() => {
|
|
157
|
+
const name = value.slice(1);
|
|
158
|
+
|
|
159
|
+
if (element.id.includes('id_form-')) {
|
|
160
|
+
const [id_prefix] = element.id.match(/id_form-[0-9]+-/i, '');
|
|
161
|
+
return document.querySelector(`#${id_prefix}${name}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
* If the element is in a table row with a class containing "dynamic-formset" we need to find the
|
|
166
|
+
* reference field in the same row.
|
|
167
|
+
*/
|
|
168
|
+
if (element.closest('tr')?.classList.contains('dynamic-formset')) {
|
|
169
|
+
return element.closest('tr').querySelector(`select[id*="${name}"]`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return document.querySelector(`#id_${name}`);
|
|
173
|
+
})()
|
|
174
|
+
: null;
|
|
175
|
+
|
|
176
|
+
const ref_field_value = ref_field
|
|
177
|
+
? (() => {
|
|
178
|
+
const field_value = getValue(ref_field);
|
|
179
|
+
const style = window.getComputedStyle(ref_field);
|
|
151
180
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
? (() => {
|
|
179
|
-
const field_value = getValue(ref_field);
|
|
180
|
-
const style = window.getComputedStyle(ref_field);
|
|
181
|
-
|
|
182
|
-
if (field_value && style.opacity !== '0' && style.visibility !== 'hidden') {
|
|
183
|
-
return field_value;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (ref_field.getAttribute('required') && ref_field.getAttribute('data-null-option')) {
|
|
187
|
-
return 'null';
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return undefined;
|
|
191
|
-
})()
|
|
192
|
-
: null;
|
|
193
|
-
|
|
194
|
-
const param_value = has_ref_field ? ref_field_value : value;
|
|
195
|
-
return param_value !== null && param_value !== undefined
|
|
196
|
-
? [[param_name, ref_field_value || value]]
|
|
197
|
-
: [];
|
|
198
|
-
});
|
|
199
|
-
}),
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
const parameters = {
|
|
203
|
-
depth: String(depth),
|
|
204
|
-
limit: String(limit),
|
|
205
|
-
offset: String(offset),
|
|
206
|
-
...(api_version ? { api_version } : undefined),
|
|
207
|
-
...(content_type ? { content_type } : undefined),
|
|
208
|
-
...(q ? { [search_field]: q } : undefined),
|
|
209
|
-
...extra_query_parameters,
|
|
210
|
-
};
|
|
181
|
+
if (field_value && style.opacity !== '0' && style.visibility !== 'hidden') {
|
|
182
|
+
return field_value;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (ref_field.getAttribute('required') && ref_field.getAttribute('data-null-option')) {
|
|
186
|
+
return 'null';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return undefined;
|
|
190
|
+
})()
|
|
191
|
+
: null;
|
|
192
|
+
|
|
193
|
+
const param_value = has_ref_field ? ref_field_value : value;
|
|
194
|
+
return param_value !== null && param_value !== undefined ? [[param_name, ref_field_value || value]] : [];
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const parameters = [
|
|
199
|
+
['depth', String(depth)],
|
|
200
|
+
['limit', String(limit)],
|
|
201
|
+
['offset', String(offset)],
|
|
202
|
+
...(api_version ? [['api_version', api_version]] : []),
|
|
203
|
+
...(content_type ? [['content_type', content_type]] : []),
|
|
204
|
+
...(q ? [[search_field, q]] : []),
|
|
205
|
+
...extra_query_parameters_array,
|
|
206
|
+
];
|
|
211
207
|
|
|
212
208
|
// This will handle params with multiple values (i.e. for list filter forms).
|
|
213
209
|
return new URLSearchParams(parameters).toString();
|
nautobot/ui/src/js/theme.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Modal } from 'bootstrap';
|
|
2
|
+
import { getCookie, setCookie } from './cookie.js';
|
|
2
3
|
|
|
3
4
|
const THEME_MODAL_ID = 'theme_modal';
|
|
4
5
|
|
|
@@ -6,70 +7,120 @@ const THEME_DARK = 'dark';
|
|
|
6
7
|
const THEME_LIGHT = 'light';
|
|
7
8
|
const THEME_SYSTEM = 'system';
|
|
8
9
|
|
|
10
|
+
const THEME_CHOICE_COOKIE_NAME = 'theme_choice';
|
|
11
|
+
const THEME_DETERMINED_COOKIE_NAME = 'theme';
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
14
|
+
* Check if given `theme_choice` is a valid theme choice, i.e. `'dark'`, `'light'`, or `'system'`:
|
|
15
|
+
* - 'dark' for always dark mode
|
|
16
|
+
* - 'light' for always light mode
|
|
17
|
+
* - 'system' to follow the system/browser preference, getPreferredColorScheme() will determine actual theme
|
|
18
|
+
* @param {string} theme_choice - Choice in question.
|
|
19
|
+
* @returns {boolean} `true` is given `theme_choice` is a valid Nautobot theme, `false` otherwise.
|
|
20
|
+
*/
|
|
21
|
+
const isValidThemeChoice = (theme_choice) => [THEME_DARK, THEME_LIGHT, THEME_SYSTEM].includes(theme_choice);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get preferred system color scheme from browser/OS settings.
|
|
11
25
|
* @returns {('dark'|'light')} Preferred system color scheme.
|
|
12
26
|
*/
|
|
13
|
-
const
|
|
27
|
+
const determineBrowserPreference = () =>
|
|
14
28
|
window.matchMedia?.(`(prefers-color-scheme: ${THEME_DARK})`).matches ? THEME_DARK : THEME_LIGHT;
|
|
15
29
|
|
|
16
30
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @
|
|
19
|
-
* @returns {boolean} `true` is given `theme` is a valid Nautobot theme, `false` otherwise.
|
|
31
|
+
* Determine the effective theme to be used.
|
|
32
|
+
* @returns {('dark'|'light')} The effective theme.
|
|
20
33
|
*/
|
|
21
|
-
const
|
|
34
|
+
const determineTheme = () => {
|
|
35
|
+
const current_theme_choice = getCookie(THEME_CHOICE_COOKIE_NAME);
|
|
36
|
+
if ([THEME_DARK, THEME_LIGHT].includes(current_theme_choice)) {
|
|
37
|
+
// An explicit choice of dark or light mode.
|
|
38
|
+
return current_theme_choice;
|
|
39
|
+
}
|
|
40
|
+
// Follow the system/browser preference by default ('system' choice or invalid choice).
|
|
41
|
+
return determineBrowserPreference();
|
|
42
|
+
};
|
|
22
43
|
|
|
23
44
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* @returns {
|
|
45
|
+
* Persist the user's theme choice in cookie.
|
|
46
|
+
* @param {('dark'|'light'|'system')} theme - The user's theme choice.
|
|
47
|
+
* @returns {void} Do not return any value
|
|
27
48
|
*/
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return cookieTheme;
|
|
49
|
+
const persistThemeChoice = (theme) => {
|
|
50
|
+
if (isValidThemeChoice(theme)) {
|
|
51
|
+
setCookie(THEME_CHOICE_COOKIE_NAME, theme, { 'max-age': 31536000, path: '/' }); // 1 year
|
|
32
52
|
}
|
|
53
|
+
};
|
|
33
54
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Handle syntax highlighter theme change.
|
|
57
|
+
* @param {('dark'|'light')} theme - The effective theme.
|
|
58
|
+
* @returns {void} Do not return any value
|
|
59
|
+
*/
|
|
60
|
+
const handleSyntaxHighlighterThemeChange = (theme) => {
|
|
61
|
+
// Since the syntax highlighter stylesheet is loaded via a <link> tag, we need to swap out the href to change themes.
|
|
62
|
+
// The best fix would be to have both stylesheets loaded and use media queries to switch between them, similar to how we do for the main Nautobot theme.
|
|
63
|
+
const highlighterLinks = document.querySelectorAll('link[rel="stylesheet"][href*="github"]');
|
|
64
|
+
if (highlighterLinks.length === 1) {
|
|
65
|
+
// We send two stylesheets on the initial page with media queries, but after that only one is present which we need to swap out.
|
|
66
|
+
const [syntaxLinkElement] = highlighterLinks;
|
|
67
|
+
const css_file = theme === THEME_DARK ? 'github-dark.min.css' : 'github.min.css';
|
|
68
|
+
syntaxLinkElement.href = syntaxLinkElement.href.replace(/github(-dark)?\.min\.css/, css_file);
|
|
37
69
|
}
|
|
70
|
+
};
|
|
38
71
|
|
|
39
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Handle Echarts theme change.
|
|
74
|
+
* @param {('dark'|'light')} theme - The effective theme.
|
|
75
|
+
* @returns {void} Do not return any value
|
|
76
|
+
*/
|
|
77
|
+
const handleEchartsThemeChange = (theme) => {
|
|
78
|
+
// If using Echarts, we need to update the theme there as well.
|
|
79
|
+
const echart_instances = document.querySelectorAll('div[_echarts_instance_]');
|
|
80
|
+
echart_instances?.forEach((instance) => {
|
|
81
|
+
const options = JSON.parse(document.getElementById(`echarts-config-${instance.id}`).textContent);
|
|
82
|
+
const colors = Array.isArray(options.color)
|
|
83
|
+
? options.color.map((colorObj) => colorObj?.[theme] || colorObj?.light || colorObj)
|
|
84
|
+
: options.color;
|
|
85
|
+
window.echarts.getInstanceByDom(instance)?.setOption({
|
|
86
|
+
color: colors,
|
|
87
|
+
darkMode: theme === THEME_DARK,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
40
90
|
};
|
|
41
91
|
|
|
42
92
|
/**
|
|
43
93
|
* Set Nautobot theme.
|
|
44
|
-
* @param {('dark'|'light'|'system')} theme - Nautobot theme to be set.
|
|
45
|
-
* @param {{ manual?: boolean }} [options] - Setter function options object. Currently supported option is `manual`.
|
|
94
|
+
* @param {('dark'|'light'|'system'|null)} theme - Nautobot theme to be set. If `null`, determine theme based on existing user choice or system preference.
|
|
46
95
|
* @returns {void} Do not return any value, set given `theme` and save it into a persistent store if `manual` is `true`.
|
|
47
96
|
*/
|
|
48
|
-
const setTheme = (theme
|
|
49
|
-
|
|
97
|
+
const setTheme = (theme = null) => {
|
|
98
|
+
if (theme !== null && isValidThemeChoice(theme)) {
|
|
99
|
+
// An explicit theme was provided, so we persist the choice.
|
|
100
|
+
persistThemeChoice(theme);
|
|
101
|
+
}
|
|
102
|
+
const current_theme_choice = getCookie(THEME_CHOICE_COOKIE_NAME); // The user's choice for the purpose of button highlighting.
|
|
103
|
+
const determined_theme = determineTheme(); // The effective theme to be applied.
|
|
104
|
+
|
|
105
|
+
// Persist the determined theme for server-side rendering purposes.
|
|
106
|
+
setCookie(THEME_DETERMINED_COOKIE_NAME, determined_theme, { 'max-age': 31536000, path: '/' }); // 1 year
|
|
50
107
|
|
|
51
108
|
const modal = document.getElementById(THEME_MODAL_ID);
|
|
52
109
|
const buttons = modal?.querySelectorAll('button[data-nb-theme]') ?? [];
|
|
53
110
|
|
|
54
111
|
buttons.forEach((button) =>
|
|
55
112
|
['border', 'border-primary'].forEach((className) =>
|
|
56
|
-
button.classList.toggle(className, button.dataset.nbTheme ===
|
|
113
|
+
button.classList.toggle(className, button.dataset.nbTheme === current_theme_choice),
|
|
57
114
|
),
|
|
58
115
|
);
|
|
59
116
|
|
|
60
|
-
|
|
61
|
-
document.documentElement.dataset.
|
|
62
|
-
document.documentElement.dataset.bsTheme = bsTheme;
|
|
117
|
+
document.documentElement.dataset.theme = determined_theme;
|
|
118
|
+
document.documentElement.dataset.bsTheme = determined_theme;
|
|
63
119
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
window.localStorage?.removeItem('theme');
|
|
67
|
-
} else if (isManual) {
|
|
68
|
-
setCookie('theme', theme);
|
|
69
|
-
window.localStorage?.setItem('theme', theme);
|
|
70
|
-
}
|
|
120
|
+
handleSyntaxHighlighterThemeChange(determined_theme);
|
|
121
|
+
handleEchartsThemeChange(determined_theme);
|
|
71
122
|
|
|
72
|
-
if (
|
|
123
|
+
if (determined_theme === THEME_DARK) {
|
|
73
124
|
[...document.getElementsByTagName('object')].forEach((object) => {
|
|
74
125
|
object.addEventListener('load', (event) => {
|
|
75
126
|
if (event.target.contentDocument) {
|
|
@@ -84,20 +135,59 @@ const setTheme = (theme, options) => {
|
|
|
84
135
|
});
|
|
85
136
|
});
|
|
86
137
|
}
|
|
138
|
+
};
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
|
|
140
|
+
const handleThemeChoiceUpgradeAndSetDefault = () => {
|
|
141
|
+
/**
|
|
142
|
+
* We need to handle the "upgrade" scenario:
|
|
143
|
+
* - Prior to Nautobot 3.0, the `theme` cookie was a Session cookie so not persisted.
|
|
144
|
+
* - localStorage was where a user's choice was "stored"
|
|
145
|
+
*
|
|
146
|
+
* In Nautobot 3.0+, we will do away with localStorage and instead move the user's choice to a persistent cookie, `theme_choice`.
|
|
147
|
+
* We will also keep the `theme` cookie as the "determined" theme for server-side rendering purposes.
|
|
148
|
+
* Generally we shouldn't need this as good media queries should handle it, but it's here for completeness.
|
|
149
|
+
* For example, some SVG renderings on the server may need to know the theme.
|
|
150
|
+
*
|
|
151
|
+
* To handle this upgrade scenario, we will:
|
|
152
|
+
* 1. Check if `theme_choice` cookie exists. If it does, we assume the user has already "upgraded" and do nothing.
|
|
153
|
+
* 2. If `theme_choice` cookie does not exist, we check localStorage for the user's theme choice.
|
|
154
|
+
* 3. If localStorage has a valid theme choice, we persist it to the `theme_choice` cookie.
|
|
155
|
+
* 4. Finally, we remove the theme from localStorage to complete the migration.'
|
|
156
|
+
*
|
|
157
|
+
* We can remove/refactor this in 4.0 for certain, but likely in 3.1+.
|
|
158
|
+
*
|
|
159
|
+
* In that scenario, if the theme_choice cookie is missing, we can default to 'system' without checking localStorage.
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
const themeChoiceCookie = getCookie(THEME_CHOICE_COOKIE_NAME);
|
|
163
|
+
if (!isValidThemeChoice(themeChoiceCookie)) {
|
|
164
|
+
// If theme_choice cookie does not exist or is invalid, check localStorage
|
|
165
|
+
const localStorageTheme = window.localStorage?.getItem('theme');
|
|
166
|
+
|
|
167
|
+
// If localStorage theme is valid, we use it, else we default to 'system';
|
|
168
|
+
// Note: we always removed the localStorage to imply the 'system' choice so technically we should only check for 'dark' or 'light' here, but we validate just in case.
|
|
169
|
+
const theme_choice_to_set = isValidThemeChoice(localStorageTheme) ? localStorageTheme : THEME_SYSTEM;
|
|
170
|
+
|
|
171
|
+
setCookie(THEME_CHOICE_COOKIE_NAME, theme_choice_to_set, { 'max-age': 31536000, path: '/' }); // 1 year
|
|
90
172
|
}
|
|
173
|
+
// Else: If theme_choice cookie exists and is valid, we don't need to do any migration.
|
|
174
|
+
// Regardless, we remove localStorage to complete migration, on the off chance it still exists.
|
|
175
|
+
window.localStorage?.removeItem('theme');
|
|
91
176
|
};
|
|
92
177
|
|
|
93
178
|
export const initializeTheme = () => {
|
|
94
179
|
const modal = document.getElementById(THEME_MODAL_ID);
|
|
95
180
|
const buttons = modal?.querySelectorAll('button[data-nb-theme]') ?? [];
|
|
96
181
|
|
|
97
|
-
|
|
98
|
-
|
|
182
|
+
handleThemeChoiceUpgradeAndSetDefault();
|
|
183
|
+
setTheme();
|
|
184
|
+
|
|
185
|
+
window.matchMedia(`(prefers-color-scheme: ${THEME_DARK})`).addEventListener('change', () => setTheme());
|
|
99
186
|
|
|
100
|
-
const onClick = (event) =>
|
|
187
|
+
const onClick = (event) => {
|
|
188
|
+
setTheme(event.currentTarget.dataset.nbTheme);
|
|
189
|
+
Modal.getInstance(modal).hide();
|
|
190
|
+
};
|
|
101
191
|
buttons.forEach((button) => button.addEventListener('click', onClick));
|
|
102
192
|
|
|
103
193
|
return () => buttons.forEach((button) => button.removeEventListener('click', onClick));
|
|
@@ -114,6 +114,7 @@ $dark-border-subtle-dark: $gray-05-dark;
|
|
|
114
114
|
/* Options */
|
|
115
115
|
$enable-shadows: true;
|
|
116
116
|
$enable-caret: false;
|
|
117
|
+
$enable-smooth-scroll: false;
|
|
117
118
|
$enable-negative-margins: true;
|
|
118
119
|
|
|
119
120
|
$prefix: bs-;
|
|
@@ -419,6 +420,8 @@ $btn-close-filter-dark: invert(100%); /* $black-0-dark: #ffffff; */
|
|
|
419
420
|
$btn-close-white-filter: $btn-close-filter-dark;
|
|
420
421
|
|
|
421
422
|
/* Code */
|
|
423
|
+
$code-font-size: $font-size-base;
|
|
424
|
+
|
|
422
425
|
$kbd-padding-y: map.get($spacers, 1);
|
|
423
426
|
$kbd-padding-x: map.get($spacers, 4);
|
|
424
427
|
$kbd-color: var(--#{$prefix}secondary-color);
|
|
@@ -775,11 +778,13 @@ button {
|
|
|
775
778
|
|
|
776
779
|
> thead:not(caption) > tr > th {
|
|
777
780
|
background-color: var(--#{$prefix}tertiary-bg);
|
|
778
|
-
color: var(--#{$prefix}
|
|
781
|
+
color: var(--#{$prefix}tertiary-color);
|
|
779
782
|
position: relative;
|
|
780
783
|
|
|
781
784
|
&.nb-actionable,
|
|
782
785
|
&.orderable {
|
|
786
|
+
color: var(--#{$prefix}secondary-color);
|
|
787
|
+
|
|
783
788
|
a,
|
|
784
789
|
button {
|
|
785
790
|
&:empty {
|
|
@@ -1454,9 +1459,14 @@ textarea {
|
|
|
1454
1459
|
}
|
|
1455
1460
|
|
|
1456
1461
|
/* Code */
|
|
1462
|
+
:not(pre) > code:not(.hljs) { /* Select only `code` which are not immediate children of `pre`, that is _inline code_. */
|
|
1463
|
+
@extend kbd;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1457
1466
|
pre:not(:has(code.hljs)) { /* Do not include highlight.js in this rule because it has its own set of styles. */
|
|
1458
1467
|
background: var(--#{$prefix}secondary-bg);
|
|
1459
1468
|
border-radius: $border-radius;
|
|
1469
|
+
color: $kbd-color;
|
|
1460
1470
|
padding-block: map.get($spacers, 8);
|
|
1461
1471
|
padding-inline: map.get($spacers, 10);
|
|
1462
1472
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
<div class="card">
|
|
22
22
|
<div class="card-header"><strong>Phase 1 Policy Assignments</strong></div>
|
|
23
|
-
<div class="card-body">
|
|
23
|
+
<div class="card-body overflow-auto">
|
|
24
24
|
{% if vpn_phase1_policies.errors %}
|
|
25
25
|
<div class="text-danger">
|
|
26
26
|
Please correct the error(s) below:
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
</div>
|
|
73
73
|
<div class="card">
|
|
74
74
|
<div class="card-header"><strong>Phase 2 Policy Assignments</strong></div>
|
|
75
|
-
<div class="card-body">
|
|
75
|
+
<div class="card-body overflow-auto">
|
|
76
76
|
{% if vpn_phase2_policies.errors %}
|
|
77
77
|
<div class="text-danger">
|
|
78
78
|
Please correct the error(s) below:
|