diff --git a/netbox/project-static/.eslintrc b/netbox/project-static/.eslintrc index 242b88dde..802fa7a3e 100644 --- a/netbox/project-static/.eslintrc +++ b/netbox/project-static/.eslintrc @@ -6,7 +6,7 @@ "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", - "prettier/@typescript-eslint" + "prettier" ], "parser": "@typescript-eslint/parser", "env": { @@ -19,8 +19,7 @@ "sourceType": "module", "ecmaFeatures": { "arrowFunctions": true - }, - "project": "./tsconfig.json" + } }, "plugins": ["@typescript-eslint", "prettier"], "settings": { @@ -35,7 +34,7 @@ "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars-experimental": "error", "no-unused-vars": "off", - + "no-inner-declarations": "off", "comma-dangle": ["error", "always-multiline"], "global-require": "off", "import/no-dynamic-require": "off", diff --git a/netbox/project-static/dist/config.js b/netbox/project-static/dist/config.js index 291dd4aeb..1763a8943 100644 Binary files a/netbox/project-static/dist/config.js and b/netbox/project-static/dist/config.js differ diff --git a/netbox/project-static/dist/config.js.map b/netbox/project-static/dist/config.js.map index e843c88ba..66a880712 100644 Binary files a/netbox/project-static/dist/config.js.map and b/netbox/project-static/dist/config.js.map differ diff --git a/netbox/project-static/dist/jobs.js b/netbox/project-static/dist/jobs.js index e8ca9e45a..c0c05058d 100644 Binary files a/netbox/project-static/dist/jobs.js and b/netbox/project-static/dist/jobs.js differ diff --git a/netbox/project-static/dist/jobs.js.map b/netbox/project-static/dist/jobs.js.map index 98a83549c..9843e27a6 100644 Binary files a/netbox/project-static/dist/jobs.js.map and b/netbox/project-static/dist/jobs.js.map differ diff --git a/netbox/project-static/dist/lldp.js b/netbox/project-static/dist/lldp.js index 823621db2..f2c9aa411 100644 Binary files a/netbox/project-static/dist/lldp.js and b/netbox/project-static/dist/lldp.js differ diff --git a/netbox/project-static/dist/lldp.js.map b/netbox/project-static/dist/lldp.js.map index 2a6e25806..beaa000bc 100644 Binary files a/netbox/project-static/dist/lldp.js.map and b/netbox/project-static/dist/lldp.js.map differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index c55ebf672..e1ffeadbc 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index 1918ceb17..0314df851 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/dist/status.js b/netbox/project-static/dist/status.js index 62d937941..e9f67d191 100644 Binary files a/netbox/project-static/dist/status.js and b/netbox/project-static/dist/status.js differ diff --git a/netbox/project-static/dist/status.js.map b/netbox/project-static/dist/status.js.map index 1d4d1ec9b..1d0eeebd6 100644 Binary files a/netbox/project-static/dist/status.js.map and b/netbox/project-static/dist/status.js.map differ diff --git a/netbox/project-static/src/bs.ts b/netbox/project-static/src/bs.ts index ee22bf685..f31bbb9ef 100644 --- a/netbox/project-static/src/bs.ts +++ b/netbox/project-static/src/bs.ts @@ -45,12 +45,16 @@ export function createToast( switch (level) { case 'warning': iconName = 'mdi-alert'; + break; case 'success': iconName = 'mdi-check-circle'; + break; case 'info': iconName = 'mdi-information'; + break; case 'danger': iconName = 'mdi-alert'; + break; } const container = document.createElement('div'); @@ -109,7 +113,7 @@ export function createToast( */ function initTabs() { const { hash } = location; - if (hash && hash.match(/^\#tab_.+$/)) { + if (hash && hash.match(/^#tab_.+$/)) { // The tab element will have a data-bs-target attribute with a value of the object type for // the corresponding tab. Once we drop the `tab_` prefix, the hash will match the target // element's data-bs-target value. For example, `#tab_frontports` becomes `#frontports`. diff --git a/netbox/project-static/src/buttons.ts b/netbox/project-static/src/buttons.ts index 7527bbb9c..da3a4aec0 100644 --- a/netbox/project-static/src/buttons.ts +++ b/netbox/project-static/src/buttons.ts @@ -21,7 +21,7 @@ type ObjectDepthState = { hidden: boolean }; * * @param element Connection Toggle Button Element */ -function toggleConnection(element: HTMLButtonElement) { +function toggleConnection(element: HTMLButtonElement): void { const id = element.getAttribute('data'); const connected = element.classList.contains('connected'); const status = connected ? 'planned' : 'connected'; @@ -59,7 +59,7 @@ function toggleConnection(element: HTMLButtonElement) { } } -function initConnectionToggle() { +function initConnectionToggle(): void { for (const element of getElements('button.cable-toggle')) { element.addEventListener('click', () => toggleConnection(element)); } @@ -116,7 +116,7 @@ function handleDepthToggle(state: StateManager, button: HTMLBu /** * Initialize object depth toggle buttons. */ -function initDepthToggle() { +function initDepthToggle(): void { const initiallyHidden = objectDepthState.get('hidden'); for (const button of getElements('button.toggle-depth')) { @@ -190,7 +190,7 @@ function handlePreferenceSave(event: Event): void { /** * Initialize handlers for user profile updates. */ -function initPreferenceUpdate() { +function initPreferenceUpdate(): void { const form = getElement('preferences-update'); if (form !== null) { form.addEventListener('submit', handlePreferenceSave); @@ -203,7 +203,7 @@ function initPreferenceUpdate() { * * @param event Change Event */ -function handleSelectAllToggle(event: Event) { +function handleSelectAllToggle(event: Event): void { // Select all checkbox in header row. const tableSelectAll = event.currentTarget as HTMLInputElement; // Nearest table to the select all checkbox. @@ -248,7 +248,7 @@ function handleSelectAllToggle(event: Event) { * * @param event Change Event */ -function handlePkCheck(event: Event) { +function handlePkCheck(event: Event): void { const target = event.currentTarget as HTMLInputElement; if (!target.checked) { for (const element of getElements( @@ -267,7 +267,7 @@ function handlePkCheck(event: Event) { * * @param event Change Event */ -function handleSelectAll(event: Event) { +function handleSelectAll(event: Event): void { const target = event.currentTarget as HTMLInputElement; const selectAllBox = getElement('select-all-box'); if (selectAllBox !== null) { @@ -286,7 +286,7 @@ function handleSelectAll(event: Event) { /** * Initialize table select all elements. */ -function initSelectAll() { +function initSelectAll(): void { for (const element of getElements( 'table tr th > input[type="checkbox"].toggle', )) { @@ -302,20 +302,20 @@ function initSelectAll() { } } -function handlePerPageSelect(event: Event) { +function handlePerPageSelect(event: Event): void { const select = event.currentTarget as HTMLSelectElement; if (select.form !== null) { select.form.submit(); } } -function initPerPage() { +function initPerPage(): void { for (const element of getElements('select.per-page')) { element.addEventListener('change', handlePerPageSelect); } } -export function initButtons() { +export function initButtons(): void { for (const func of [ initDepthToggle, initConnectionToggle, diff --git a/netbox/project-static/src/clipboard.ts b/netbox/project-static/src/clipboard.ts index f83936346..a04acba39 100644 --- a/netbox/project-static/src/clipboard.ts +++ b/netbox/project-static/src/clipboard.ts @@ -1,7 +1,7 @@ import Clipboard from 'clipboard'; import { getElements } from './util'; -export function initClipboard() { +export function initClipboard(): void { for (const element of getElements('a.copy-token', 'button.copy-secret')) { new Clipboard(element); } diff --git a/netbox/project-static/src/colorMode.ts b/netbox/project-static/src/colorMode.ts index 443e0b6fe..dfd05df4f 100644 --- a/netbox/project-static/src/colorMode.ts +++ b/netbox/project-static/src/colorMode.ts @@ -6,7 +6,10 @@ const TEXT_WHEN_LIGHT = 'Dark Mode'; const ICON_WHEN_DARK = 'mdi-lightbulb-on'; const ICON_WHEN_LIGHT = 'mdi-lightbulb'; -function isColorMode(value: string): value is ColorMode { +/** + * Determine if a value is a supported color mode string value. + */ +function isColorMode(value: unknown): value is ColorMode { return value === 'dark' || value === 'light'; } diff --git a/netbox/project-static/src/device/config.ts b/netbox/project-static/src/device/config.ts index a4898ba8e..cbe70952e 100644 --- a/netbox/project-static/src/device/config.ts +++ b/netbox/project-static/src/device/config.ts @@ -4,7 +4,7 @@ import { apiGetBase, getNetboxData, hasError, toggleLoader } from '../util'; /** * Initialize device config elements. */ -function initConfig() { +function initConfig(): void { toggleLoader('show'); const url = getNetboxData('data-object-url'); diff --git a/netbox/project-static/src/device/lldp.ts b/netbox/project-static/src/device/lldp.ts index 71a8854df..6baaa9b38 100644 --- a/netbox/project-static/src/device/lldp.ts +++ b/netbox/project-static/src/device/lldp.ts @@ -31,7 +31,7 @@ function updateRowStyle(data: LLDPNeighborDetail) { let cInterfaceShort = null; if (isTruthy(cInterface)) { - cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9\/]+)$/, '$1$2'); + cInterfaceShort = cInterface.replace(/^([A-Z][a-z])[^0-9]*([0-9/]+)$/, '$1$2'); } const nHost = neighbor.remote_system_name ?? ''; diff --git a/netbox/project-static/src/device/status.ts b/netbox/project-static/src/device/status.ts index b06d17aa9..8261ebc82 100644 --- a/netbox/project-static/src/device/status.ts +++ b/netbox/project-static/src/device/status.ts @@ -92,7 +92,7 @@ function getUptime(seconds: number): Uptime { * * @param facts NAPALM Device Facts */ -function processFacts(facts: DeviceFacts) { +function processFacts(facts: DeviceFacts): void { for (const key of factKeys) { if (key in facts) { // Find the target element which should have its innerHTML/innerText set to a NAPALM value. @@ -149,7 +149,7 @@ function insertTitleRow(next: E, title1: string, title2: * @param next Next adjacent element.For example, if this is the CPU data, `next` would be the * memory row. */ -function insertNoneRow>(next: E) { +function insertNoneRow>(next: E): void { const none = createElement('td', { colSpan: '2', innerText: 'No Data' }, [ 'text-muted', 'text-center', @@ -173,7 +173,7 @@ function getNext(id: string): Nullable { * * @param cpu NAPALM CPU data. */ -function processCpu(cpu: DeviceEnvironment['cpu']) { +function processCpu(cpu: DeviceEnvironment['cpu']): void { // Find the next adjacent element, so we can insert elements before it. const next = getNext('status-cpu'); if (typeof cpu !== 'undefined') { @@ -200,7 +200,7 @@ function processCpu(cpu: DeviceEnvironment['cpu']) { * * @param mem NAPALM memory data. */ -function processMemory(mem: DeviceEnvironment['memory']) { +function processMemory(mem: DeviceEnvironment['memory']): void { // Find the next adjacent element, so we can insert elements before it. const next = getNext('status-memory'); if (typeof mem !== 'undefined') { @@ -222,7 +222,7 @@ function processMemory(mem: DeviceEnvironment['memory']) { * * @param temp NAPALM temperature data. */ -function processTemp(temp: DeviceEnvironment['temperature']) { +function processTemp(temp: DeviceEnvironment['temperature']): void { // Find the next adjacent element, so we can insert elements before it. const next = getNext('status-temperature'); if (typeof temp !== 'undefined') { @@ -249,7 +249,7 @@ function processTemp(temp: DeviceEnvironment['temperature']) { * * @param fans NAPALM fan data. */ -function processFans(fans: DeviceEnvironment['fans']) { +function processFans(fans: DeviceEnvironment['fans']): void { // Find the next adjacent element, so we can insert elements before it. const next = getNext('status-fans'); if (typeof fans !== 'undefined') { @@ -285,7 +285,7 @@ function processFans(fans: DeviceEnvironment['fans']) { * * @param power NAPALM power data. */ -function processPower(power: DeviceEnvironment['power']) { +function processPower(power: DeviceEnvironment['power']): void { // Find the next adjacent element, so we can insert elements before it. const next = getNext('status-power'); if (typeof power !== 'undefined') { @@ -322,7 +322,7 @@ function processPower(power: DeviceEnvironment['power']) { * * @param env NAPALM Device Environment */ -function processEnvironment(env: DeviceEnvironment) { +function processEnvironment(env: DeviceEnvironment): void { const { cpu, memory, temperature, fans, power } = env; processCpu(cpu); processMemory(memory); @@ -334,7 +334,7 @@ function processEnvironment(env: DeviceEnvironment) { /** * Initialize NAPALM device status handlers. */ -function initStatus() { +function initStatus(): void { // Show loading state for both Facts & Environment cards. toggleLoader('show'); diff --git a/netbox/project-static/src/forms.ts b/netbox/project-static/src/forms.ts index 09799856b..1dfd20cb5 100644 --- a/netbox/project-static/src/forms.ts +++ b/netbox/project-static/src/forms.ts @@ -136,7 +136,7 @@ function initFormElements() { function moveOptionUp(element: HTMLSelectElement): void { const options = Array.from(element.options); for (let i = 1; i < options.length; i++) { - let option = options[i]; + const option = options[i]; if (option.selected) { element.removeChild(option); element.insertBefore(option, element.options[i - 1]); @@ -290,7 +290,7 @@ function initScopeSelector() { } } -export function initForms() { +export function initForms(): void { for (const func of [ initFormElements, initFormActions, diff --git a/netbox/project-static/src/global.d.ts b/netbox/project-static/src/global.d.ts index 3bb06ff43..f3dd7edd9 100644 --- a/netbox/project-static/src/global.d.ts +++ b/netbox/project-static/src/global.d.ts @@ -33,6 +33,8 @@ interface Window { */ type Index = K extends string ? K : never; +type APIResponse = T | ErrorBase | APIError; + type APIAnswer = { count: number; next: Nullable; diff --git a/netbox/project-static/src/jobs.ts b/netbox/project-static/src/jobs.ts index 319910a1e..8a8a3fd12 100644 --- a/netbox/project-static/src/jobs.ts +++ b/netbox/project-static/src/jobs.ts @@ -44,10 +44,13 @@ function updateLabel(status: JobStatus) { switch (status.value) { case 'failed' || 'errored': labelClass = 'danger'; + break; case 'running': labelClass = 'warning'; + break; case 'completed': labelClass = 'success'; + break; } element.setAttribute('class', `badge bg-${labelClass}`); element.innerText = status.label; diff --git a/netbox/project-static/src/links.ts b/netbox/project-static/src/links.ts index 5752e9c48..d39f3605b 100644 --- a/netbox/project-static/src/links.ts +++ b/netbox/project-static/src/links.ts @@ -3,7 +3,7 @@ import { isTruthy, getElements } from './util'; /** * Allow any element to be made "clickable" with the use of the `data-href` attribute. */ -export function initLinks() { +export function initLinks(): void { for (const link of getElements('*[data-href]')) { const href = link.getAttribute('data-href'); if (isTruthy(href)) { diff --git a/netbox/project-static/src/netbox.ts b/netbox/project-static/src/netbox.ts index 0b61fc8e3..79c196b96 100644 --- a/netbox/project-static/src/netbox.ts +++ b/netbox/project-static/src/netbox.ts @@ -13,7 +13,7 @@ import { initSideNav } from './sidenav'; import { initRackElevation } from './racks'; import { initLinks } from './links'; -function initDocument() { +function initDocument(): void { for (const init of [ initBootstrap, initColorMode, @@ -34,7 +34,7 @@ function initDocument() { } } -function initWindow() { +function initWindow(): void { const contentContainer = document.querySelector('.content-container'); if (contentContainer !== null) { // Focus the content container for accessible navigation. diff --git a/netbox/project-static/src/racks.ts b/netbox/project-static/src/racks.ts index ebf20a024..83d7abc14 100644 --- a/netbox/project-static/src/racks.ts +++ b/netbox/project-static/src/racks.ts @@ -67,7 +67,7 @@ function handleRackImageToggle( * Add onClick callback for toggling rack elevation images. Synchronize the image toggle button * text and display state of images with the local state. */ -export function initRackElevation() { +export function initRackElevation(): void { const initiallyHidden = rackImagesState.get('hidden'); for (const button of getElements('button.toggle-images')) { toggleRackImagesButton(initiallyHidden, button); diff --git a/netbox/project-static/src/search.ts b/netbox/project-static/src/search.ts index 4eaae06e5..120d833ea 100644 --- a/netbox/project-static/src/search.ts +++ b/netbox/project-static/src/search.ts @@ -8,7 +8,7 @@ import { getElements, getRowValues, findFirstAdjacent, isTruthy } from './util'; * @param event "click" event for each dropdown item. * @param button Each dropdown item element. */ -function handleSearchDropdownClick(event: Event, button: HTMLButtonElement) { +function handleSearchDropdownClick(event: Event, button: HTMLButtonElement): void { const dropdown = event.currentTarget as HTMLButtonElement; const selectedValue = findFirstAdjacent(dropdown, 'span.search-obj-selected'); const selectedType = findFirstAdjacent(dropdown, 'input.search-obj-type'); @@ -31,7 +31,7 @@ function handleSearchDropdownClick(event: Event, button: HTMLButtonElement) { /** * Initialize Search Bar Elements. */ -function initSearchBar() { +function initSearchBar(): void { for (const dropdown of getElements('.search-obj-selector')) { for (const button of dropdown.querySelectorAll( 'li > button.dropdown-item', @@ -44,7 +44,7 @@ function initSearchBar() { /** * Initialize Interface Table Filter Elements. */ -function initInterfaceFilter() { +function initInterfaceFilter(): void { for (const input of getElements('input.interface-filter')) { const table = findFirstAdjacent(input, 'table'); const rows = Array.from( @@ -53,7 +53,7 @@ function initInterfaceFilter() { /** * Filter on-page table by input text. */ - function handleInput(event: Event) { + function handleInput(event: Event): void { const target = event.target as HTMLInputElement; // Create a regex pattern from the input search text to match against. const filter = new RegExp(target.value.toLowerCase().trim()); @@ -87,7 +87,7 @@ function initInterfaceFilter() { } } -function initTableFilter() { +function initTableFilter(): void { for (const input of getElements('input.object-filter')) { // Find the first adjacent table element. const table = findFirstAdjacent(input, 'table'); @@ -101,7 +101,7 @@ function initTableFilter() { * Filter table rows by matched input text. * @param event */ - function handleInput(event: Event) { + function handleInput(event: Event): void { const target = event.target as HTMLInputElement; // Create a regex pattern from the input search text to match against. @@ -132,7 +132,7 @@ function initTableFilter() { } } -export function initSearch() { +export function initSearch(): void { for (const func of [initSearchBar, initTableFilter, initInterfaceFilter]) { func(); } diff --git a/netbox/project-static/src/select/api.ts b/netbox/project-static/src/select/api.ts index 0e6a189ff..61c40dd8c 100644 --- a/netbox/project-static/src/select/api.ts +++ b/netbox/project-static/src/select/api.ts @@ -711,7 +711,7 @@ class APISelect { * @param id DOM ID of the other element. */ private updatePathValues(id: string): void { - let key = id.replaceAll(/^id_/gi, ''); + const key = id.replaceAll(/^id_/gi, ''); const element = getElement(`id_${key}`); if (element !== null) { // If this element's URL contains Django template tags ({{), replace the template tag @@ -982,15 +982,16 @@ class APISelect { 'button', { type: 'button' }, ['btn', 'btn-sm', 'btn-ghost-dark'], - [createElement('i', {}, ['mdi', 'mdi-reload'])], + [createElement('i', null, ['mdi', 'mdi-reload'])], ); refreshButton.addEventListener('click', () => this.loadData()); + refreshButton.type = 'button'; this.slim.slim.search.container.appendChild(refreshButton); } } } -export function initApiSelect() { +export function initApiSelect(): void { for (const select of getElements('.netbox-api-select')) { new APISelect(select); } diff --git a/netbox/project-static/src/select/color.ts b/netbox/project-static/src/select/color.ts index 9a699c47b..e2c93c37c 100644 --- a/netbox/project-static/src/select/color.ts +++ b/netbox/project-static/src/select/color.ts @@ -11,6 +11,30 @@ function canChangeColor(option: Option | HTMLOptionElement): boolean { return typeof option.value === 'string' && option.value !== ''; } +/** + * Style the container element based on the selected option value. + */ +function styleContainer( + instance: InstanceType, + option: Option | HTMLOptionElement, +): void { + if (instance.slim.singleSelected !== null) { + if (canChangeColor(option)) { + // Get the background color from the selected option's value. + const bg = `#${option.value}`; + // Determine an accessible foreground color based on the background color. + const fg = readableColor(bg); + + // Set the container's style attributes. + instance.slim.singleSelected.container.style.backgroundColor = bg; + instance.slim.singleSelected.container.style.color = fg; + } else { + // If the color cannot be set (i.e., the placeholder), remove any inline styles. + instance.slim.singleSelected.container.removeAttribute('style'); + } + } +} + /** * Initialize color selection widget. Dynamically change the style of the select container to match * the selected option. @@ -40,7 +64,7 @@ export function initColorSelect(): void { // Style the select container to match any pre-selectd options. for (const option of instance.data.data) { if ('selected' in option && option.selected) { - styleContainer(option); + styleContainer(instance, option); break; } } @@ -50,25 +74,7 @@ export function initColorSelect(): void { instance.slim.container.classList.remove(className); } - function styleContainer(option: Option | HTMLOptionElement): void { - if (instance.slim.singleSelected !== null) { - if (canChangeColor(option)) { - // Get the background color from the selected option's value. - const bg = `#${option.value}`; - // Determine an accessible foreground color based on the background color. - const fg = readableColor(bg); - - // Set the container's style attributes. - instance.slim.singleSelected.container.style.backgroundColor = bg; - instance.slim.singleSelected.container.style.color = fg; - } else { - // If the color cannot be set (i.e., the placeholder), remove any inline styles. - instance.slim.singleSelected.container.removeAttribute('style'); - } - } - } - // Change the SlimSelect container's style based on the selected option. - instance.onChange = styleContainer; + instance.onChange = option => styleContainer(instance, option); } } diff --git a/netbox/project-static/src/select/index.ts b/netbox/project-static/src/select/index.ts index 480737b02..356c8004f 100644 --- a/netbox/project-static/src/select/index.ts +++ b/netbox/project-static/src/select/index.ts @@ -2,7 +2,7 @@ import { initApiSelect } from './api'; import { initColorSelect } from './color'; import { initStaticSelect } from './static'; -export function initSelect() { +export function initSelect(): void { for (const func of [initApiSelect, initColorSelect, initStaticSelect]) { func(); } diff --git a/netbox/project-static/src/select/static.ts b/netbox/project-static/src/select/static.ts index 3bbc83647..550e5ba7d 100644 --- a/netbox/project-static/src/select/static.ts +++ b/netbox/project-static/src/select/static.ts @@ -1,7 +1,7 @@ import SlimSelect from 'slim-select'; import { getElements } from '../util'; -export function initStaticSelect() { +export function initStaticSelect(): void { for (const select of getElements('.netbox-static-select')) { if (select !== null) { const label = document.querySelector(`label[for=${select.id}]`) as HTMLLabelElement; diff --git a/netbox/project-static/src/sidenav.ts b/netbox/project-static/src/sidenav.ts index 964b73c95..34c897044 100644 --- a/netbox/project-static/src/sidenav.ts +++ b/netbox/project-static/src/sidenav.ts @@ -237,7 +237,7 @@ class SideNav { '.navbar-nav .nav .nav-item a.nav-link', )) { const href = new RegExp(link.href, 'gi'); - if (Boolean(window.location.href.match(href))) { + if (window.location.href.match(href)) { yield link; } } @@ -310,7 +310,7 @@ class SideNav { } } -export function initSideNav() { +export function initSideNav(): void { for (const sidenav of getElements('.sidenav')) { new SideNav(sidenav); } diff --git a/netbox/project-static/src/tableConfig.ts b/netbox/project-static/src/tableConfig.ts index 0eb6479ae..ff12e8d68 100644 --- a/netbox/project-static/src/tableConfig.ts +++ b/netbox/project-static/src/tableConfig.ts @@ -5,7 +5,7 @@ import { getElements, apiPatch, hasError, getSelectedOptions } from './util'; * Mark each option element in the selected columns element as 'selected' so they are submitted to * the API. */ -function saveTableConfig() { +function saveTableConfig(): void { for (const element of getElements('select[name="columns"] option')) { element.selected = true; } @@ -14,7 +14,7 @@ function saveTableConfig() { /** * Delete all selected columns, which reverts the user's preferences to the default column set. */ -function resetTableConfig() { +function resetTableConfig(): void { for (const element of getElements('select[name="columns"]')) { element.value = ''; } @@ -23,7 +23,7 @@ function resetTableConfig() { /** * Add columns to the table config select element. */ -function addColumns(event: Event) { +function addColumns(event: Event): void { for (const selectedOption of getElements('#id_available_columns > option')) { if (selectedOption.selected) { for (const selected of getElements('#id_columns')) { @@ -38,7 +38,7 @@ function addColumns(event: Event) { /** * Remove columns from the table config select element. */ -function removeColumns(event: Event) { +function removeColumns(event: Event): void { for (const selectedOption of getElements('#id_columns > option')) { if (selectedOption.selected) { for (const available of getElements('#id_available_columns')) { @@ -53,7 +53,7 @@ function removeColumns(event: Event) { /** * Submit form configuration to the NetBox API. */ -async function submitFormConfig(formConfig: Dict) { +async function submitFormConfig(formConfig: Dict): Promise> { return await apiPatch('/api/users/config/', formConfig); } @@ -61,7 +61,7 @@ async function submitFormConfig(formConfig: Dict) { * Handle table config form submission. Sends the selected columns to the NetBox API to update * the user's table configuration preferences. */ -function handleSubmit(event: Event) { +function handleSubmit(event: Event): void { event.preventDefault(); const element = event.currentTarget as HTMLFormElement; @@ -96,7 +96,7 @@ function handleSubmit(event: Event) { /** * Initialize table configuration elements. */ -export function initTableConfig() { +export function initTableConfig(): void { for (const element of getElements('#save_tableconfig')) { element.addEventListener('click', saveTableConfig); } diff --git a/netbox/project-static/src/tables/interfaceTable.ts b/netbox/project-static/src/tables/interfaceTable.ts index 7fa144adc..0f7f985aa 100644 --- a/netbox/project-static/src/tables/interfaceTable.ts +++ b/netbox/project-static/src/tables/interfaceTable.ts @@ -164,18 +164,14 @@ class TableState { private table: HTMLTableElement; /** * Instance of ButtonState for the 'show/hide enabled rows' button. - * - * TS Error is expected because null handling is performed in the constructor. */ - // @ts-expect-error + // @ts-expect-error null handling is performed in the constructor private enabledButton: ButtonState; /** * Instance of ButtonState for the 'show/hide disabled rows' button. - * - * TS Error is expected because null handling is performed in the constructor. */ - // @ts-expect-error + // @ts-expect-error null handling is performed in the constructor private disabledButton: ButtonState; /** @@ -288,7 +284,7 @@ class TableState { /** * Initialize table states. */ -export function initInterfaceTable() { +export function initInterfaceTable(): void { for (const element of getElements('table')) { new TableState(element); } diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index 5c9110091..3f399b1c2 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -1,19 +1,18 @@ import Cookie from 'cookie'; -type APIRes = T | ErrorBase | APIError; type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; type ReqData = URLSearchParams | Dict | undefined | unknown; type SelectedOption = { name: string; options: string[] }; -type HTMLElementProperties = - | { - [k in keyof E]: E[k]; - } - | {}; - -type InferredProps = HTMLElementProperties< - HTMLElementTagNameMap[T] ->; +/** + * Infer valid HTMLElement props based on element name. + */ +type InferredProps< + // Element name. + T extends keyof HTMLElementTagNameMap, + // Element type. + E extends HTMLElementTagNameMap[T] = HTMLElementTagNameMap[T] +> = Partial>; export function isApiError(data: Record): data is APIError { return 'error' in data && 'exception' in data; @@ -36,9 +35,9 @@ export function hasMore(data: APIAnswer): data is APIAnswerWithNe */ export function slugify(slug: string, chars: number): string { return slug - .replace(/[^\-\.\w\s]/g, '') // Remove unneeded chars - .replace(/^[\s\.]+|[\s\.]+$/g, '') // Trim leading/trailing spaces - .replace(/[\-\.\s]+/g, '-') // Convert spaces and decimals to hyphens + .replace(/[^\-.\w\s]/g, '') // Remove unneeded chars + .replace(/^[\s.]+|[\s.]+$/g, '') // Trim leading/trailing spaces + .replace(/[-.\s]+/g, '-') // Convert spaces and decimals to hyphens .toLowerCase() // Convert to lowercase .substring(0, chars); // Trim to first chars chars } @@ -82,7 +81,7 @@ export async function apiRequest( url: string, method: Method, data?: D, -): Promise> { +): Promise> { const token = getCsrfToken(); const headers = new Headers({ 'X-CSRFToken': token }); @@ -111,18 +110,18 @@ export async function apiRequest( export async function apiPatch( url: string, data: D, -): Promise> { +): Promise> { return await apiRequest(url, 'PATCH', data); } -export async function apiGetBase(url: string): Promise> { +export async function apiGetBase(url: string): Promise> { return await apiRequest(url, 'GET'); } export async function apiPostForm( url: string, data: D, -): Promise> { +): Promise> { const body = new URLSearchParams(); for (const [k, v] of Object.entries(data)) { body.append(k, String(v)); @@ -149,7 +148,7 @@ export function getElements( export function getElements(...key: string[]): Generator; export function* getElements( ...key: (string | keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap)[] -) { +): Generator { for (const query of key) { for (const element of document.querySelectorAll(query)) { if (element !== null) { @@ -249,7 +248,7 @@ export function getNetboxData(key: string): string | null { /** * Toggle visibility of card loader. */ -export function toggleLoader(action: 'show' | 'hide') { +export function toggleLoader(action: 'show' | 'hide'): void { for (const element of getElements('div.card-overlay')) { if (action === 'show') { element.classList.remove('d-none'); @@ -316,25 +315,27 @@ export function findFirstAdjacent, + // Child element type. C extends HTMLElement = HTMLElement ->( - tag: T, - properties: InferredProps, - classes: string[], - children: C[] = [], -): HTMLElementTagNameMap[T] { +>(tag: T, properties: P | null, classes: string[], children: C[] = []): HTMLElementTagNameMap[T] { // Create the base element. const element = document.createElement(tag); - for (const k of Object.keys(properties)) { - // Add each property to the element. - const key = k as keyof HTMLElementProperties; - const value = properties[key]; - if (key in element) { - element[key] = value; + if (properties !== null) { + for (const k of Object.keys(properties)) { + // Add each property to the element. + const key = k as keyof InferredProps; + const value = properties[key] as NonNullable; + if (key in element) { + element[key] = value; + } } } + // Add each CSS class to the element's class list. element.classList.add(...classes); diff --git a/netbox/project-static/styles/overrides.scss b/netbox/project-static/styles/overrides.scss index 0e82e3f14..ae59f6d5c 100644 --- a/netbox/project-static/styles/overrides.scss +++ b/netbox/project-static/styles/overrides.scss @@ -37,4 +37,4 @@ a[type='button'] { .badge { font-size: $font-size-xs; -} \ No newline at end of file +}