Fix eslint misconfiguration and corresponding errors

This commit is contained in:
Matt 2021-08-23 22:31:36 -07:00
parent 82a209bc5b
commit a0ba8380c9
34 changed files with 130 additions and 115 deletions

View File

@ -6,7 +6,7 @@
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended", "plugin:prettier/recommended",
"prettier/@typescript-eslint" "prettier"
], ],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"env": { "env": {
@ -19,8 +19,7 @@
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
"arrowFunctions": true "arrowFunctions": true
}, }
"project": "./tsconfig.json"
}, },
"plugins": ["@typescript-eslint", "prettier"], "plugins": ["@typescript-eslint", "prettier"],
"settings": { "settings": {
@ -35,7 +34,7 @@
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-unused-vars-experimental": "error", "@typescript-eslint/no-unused-vars-experimental": "error",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-inner-declarations": "off",
"comma-dangle": ["error", "always-multiline"], "comma-dangle": ["error", "always-multiline"],
"global-require": "off", "global-require": "off",
"import/no-dynamic-require": "off", "import/no-dynamic-require": "off",

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.

View File

@ -45,12 +45,16 @@ export function createToast(
switch (level) { switch (level) {
case 'warning': case 'warning':
iconName = 'mdi-alert'; iconName = 'mdi-alert';
break;
case 'success': case 'success':
iconName = 'mdi-check-circle'; iconName = 'mdi-check-circle';
break;
case 'info': case 'info':
iconName = 'mdi-information'; iconName = 'mdi-information';
break;
case 'danger': case 'danger':
iconName = 'mdi-alert'; iconName = 'mdi-alert';
break;
} }
const container = document.createElement('div'); const container = document.createElement('div');
@ -109,7 +113,7 @@ export function createToast(
*/ */
function initTabs() { function initTabs() {
const { hash } = location; 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 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 // 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`. // element's data-bs-target value. For example, `#tab_frontports` becomes `#frontports`.

View File

@ -21,7 +21,7 @@ type ObjectDepthState = { hidden: boolean };
* *
* @param element Connection Toggle Button Element * @param element Connection Toggle Button Element
*/ */
function toggleConnection(element: HTMLButtonElement) { function toggleConnection(element: HTMLButtonElement): void {
const id = element.getAttribute('data'); const id = element.getAttribute('data');
const connected = element.classList.contains('connected'); const connected = element.classList.contains('connected');
const status = connected ? 'planned' : 'connected'; const status = connected ? 'planned' : 'connected';
@ -59,7 +59,7 @@ function toggleConnection(element: HTMLButtonElement) {
} }
} }
function initConnectionToggle() { function initConnectionToggle(): void {
for (const element of getElements<HTMLButtonElement>('button.cable-toggle')) { for (const element of getElements<HTMLButtonElement>('button.cable-toggle')) {
element.addEventListener('click', () => toggleConnection(element)); element.addEventListener('click', () => toggleConnection(element));
} }
@ -116,7 +116,7 @@ function handleDepthToggle(state: StateManager<ObjectDepthState>, button: HTMLBu
/** /**
* Initialize object depth toggle buttons. * Initialize object depth toggle buttons.
*/ */
function initDepthToggle() { function initDepthToggle(): void {
const initiallyHidden = objectDepthState.get('hidden'); const initiallyHidden = objectDepthState.get('hidden');
for (const button of getElements<HTMLButtonElement>('button.toggle-depth')) { for (const button of getElements<HTMLButtonElement>('button.toggle-depth')) {
@ -190,7 +190,7 @@ function handlePreferenceSave(event: Event): void {
/** /**
* Initialize handlers for user profile updates. * Initialize handlers for user profile updates.
*/ */
function initPreferenceUpdate() { function initPreferenceUpdate(): void {
const form = getElement<HTMLFormElement>('preferences-update'); const form = getElement<HTMLFormElement>('preferences-update');
if (form !== null) { if (form !== null) {
form.addEventListener('submit', handlePreferenceSave); form.addEventListener('submit', handlePreferenceSave);
@ -203,7 +203,7 @@ function initPreferenceUpdate() {
* *
* @param event Change Event * @param event Change Event
*/ */
function handleSelectAllToggle(event: Event) { function handleSelectAllToggle(event: Event): void {
// Select all checkbox in header row. // Select all checkbox in header row.
const tableSelectAll = event.currentTarget as HTMLInputElement; const tableSelectAll = event.currentTarget as HTMLInputElement;
// Nearest table to the select all checkbox. // Nearest table to the select all checkbox.
@ -248,7 +248,7 @@ function handleSelectAllToggle(event: Event) {
* *
* @param event Change Event * @param event Change Event
*/ */
function handlePkCheck(event: Event) { function handlePkCheck(event: Event): void {
const target = event.currentTarget as HTMLInputElement; const target = event.currentTarget as HTMLInputElement;
if (!target.checked) { if (!target.checked) {
for (const element of getElements<HTMLInputElement>( for (const element of getElements<HTMLInputElement>(
@ -267,7 +267,7 @@ function handlePkCheck(event: Event) {
* *
* @param event Change Event * @param event Change Event
*/ */
function handleSelectAll(event: Event) { function handleSelectAll(event: Event): void {
const target = event.currentTarget as HTMLInputElement; const target = event.currentTarget as HTMLInputElement;
const selectAllBox = getElement<HTMLDivElement>('select-all-box'); const selectAllBox = getElement<HTMLDivElement>('select-all-box');
if (selectAllBox !== null) { if (selectAllBox !== null) {
@ -286,7 +286,7 @@ function handleSelectAll(event: Event) {
/** /**
* Initialize table select all elements. * Initialize table select all elements.
*/ */
function initSelectAll() { function initSelectAll(): void {
for (const element of getElements<HTMLInputElement>( for (const element of getElements<HTMLInputElement>(
'table tr th > input[type="checkbox"].toggle', '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; const select = event.currentTarget as HTMLSelectElement;
if (select.form !== null) { if (select.form !== null) {
select.form.submit(); select.form.submit();
} }
} }
function initPerPage() { function initPerPage(): void {
for (const element of getElements<HTMLSelectElement>('select.per-page')) { for (const element of getElements<HTMLSelectElement>('select.per-page')) {
element.addEventListener('change', handlePerPageSelect); element.addEventListener('change', handlePerPageSelect);
} }
} }
export function initButtons() { export function initButtons(): void {
for (const func of [ for (const func of [
initDepthToggle, initDepthToggle,
initConnectionToggle, initConnectionToggle,

View File

@ -1,7 +1,7 @@
import Clipboard from 'clipboard'; import Clipboard from 'clipboard';
import { getElements } from './util'; import { getElements } from './util';
export function initClipboard() { export function initClipboard(): void {
for (const element of getElements('a.copy-token', 'button.copy-secret')) { for (const element of getElements('a.copy-token', 'button.copy-secret')) {
new Clipboard(element); new Clipboard(element);
} }

View File

@ -6,7 +6,10 @@ const TEXT_WHEN_LIGHT = 'Dark Mode';
const ICON_WHEN_DARK = 'mdi-lightbulb-on'; const ICON_WHEN_DARK = 'mdi-lightbulb-on';
const ICON_WHEN_LIGHT = 'mdi-lightbulb'; 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'; return value === 'dark' || value === 'light';
} }

View File

@ -4,7 +4,7 @@ import { apiGetBase, getNetboxData, hasError, toggleLoader } from '../util';
/** /**
* Initialize device config elements. * Initialize device config elements.
*/ */
function initConfig() { function initConfig(): void {
toggleLoader('show'); toggleLoader('show');
const url = getNetboxData('data-object-url'); const url = getNetboxData('data-object-url');

View File

@ -31,7 +31,7 @@ function updateRowStyle(data: LLDPNeighborDetail) {
let cInterfaceShort = null; let cInterfaceShort = null;
if (isTruthy(cInterface)) { 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 ?? ''; const nHost = neighbor.remote_system_name ?? '';

View File

@ -92,7 +92,7 @@ function getUptime(seconds: number): Uptime {
* *
* @param facts NAPALM Device Facts * @param facts NAPALM Device Facts
*/ */
function processFacts(facts: DeviceFacts) { function processFacts(facts: DeviceFacts): void {
for (const key of factKeys) { for (const key of factKeys) {
if (key in facts) { if (key in facts) {
// Find the target element which should have its innerHTML/innerText set to a NAPALM value. // Find the target element which should have its innerHTML/innerText set to a NAPALM value.
@ -149,7 +149,7 @@ function insertTitleRow<E extends HTMLElement>(next: E, title1: string, title2:
* @param next Next adjacent element.For example, if this is the CPU data, `next` would be the * @param next Next adjacent element.For example, if this is the CPU data, `next` would be the
* memory row. * memory row.
*/ */
function insertNoneRow<E extends Nullable<HTMLElement>>(next: E) { function insertNoneRow<E extends Nullable<HTMLElement>>(next: E): void {
const none = createElement('td', { colSpan: '2', innerText: 'No Data' }, [ const none = createElement('td', { colSpan: '2', innerText: 'No Data' }, [
'text-muted', 'text-muted',
'text-center', 'text-center',
@ -173,7 +173,7 @@ function getNext<E extends HTMLElement>(id: string): Nullable<E> {
* *
* @param cpu NAPALM CPU data. * @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. // Find the next adjacent element, so we can insert elements before it.
const next = getNext<HTMLTableRowElement>('status-cpu'); const next = getNext<HTMLTableRowElement>('status-cpu');
if (typeof cpu !== 'undefined') { if (typeof cpu !== 'undefined') {
@ -200,7 +200,7 @@ function processCpu(cpu: DeviceEnvironment['cpu']) {
* *
* @param mem NAPALM memory data. * @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. // Find the next adjacent element, so we can insert elements before it.
const next = getNext<HTMLTableRowElement>('status-memory'); const next = getNext<HTMLTableRowElement>('status-memory');
if (typeof mem !== 'undefined') { if (typeof mem !== 'undefined') {
@ -222,7 +222,7 @@ function processMemory(mem: DeviceEnvironment['memory']) {
* *
* @param temp NAPALM temperature data. * @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. // Find the next adjacent element, so we can insert elements before it.
const next = getNext<HTMLTableRowElement>('status-temperature'); const next = getNext<HTMLTableRowElement>('status-temperature');
if (typeof temp !== 'undefined') { if (typeof temp !== 'undefined') {
@ -249,7 +249,7 @@ function processTemp(temp: DeviceEnvironment['temperature']) {
* *
* @param fans NAPALM fan data. * @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. // Find the next adjacent element, so we can insert elements before it.
const next = getNext<HTMLTableRowElement>('status-fans'); const next = getNext<HTMLTableRowElement>('status-fans');
if (typeof fans !== 'undefined') { if (typeof fans !== 'undefined') {
@ -285,7 +285,7 @@ function processFans(fans: DeviceEnvironment['fans']) {
* *
* @param power NAPALM power data. * @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. // Find the next adjacent element, so we can insert elements before it.
const next = getNext<HTMLTableRowElement>('status-power'); const next = getNext<HTMLTableRowElement>('status-power');
if (typeof power !== 'undefined') { if (typeof power !== 'undefined') {
@ -322,7 +322,7 @@ function processPower(power: DeviceEnvironment['power']) {
* *
* @param env NAPALM Device Environment * @param env NAPALM Device Environment
*/ */
function processEnvironment(env: DeviceEnvironment) { function processEnvironment(env: DeviceEnvironment): void {
const { cpu, memory, temperature, fans, power } = env; const { cpu, memory, temperature, fans, power } = env;
processCpu(cpu); processCpu(cpu);
processMemory(memory); processMemory(memory);
@ -334,7 +334,7 @@ function processEnvironment(env: DeviceEnvironment) {
/** /**
* Initialize NAPALM device status handlers. * Initialize NAPALM device status handlers.
*/ */
function initStatus() { function initStatus(): void {
// Show loading state for both Facts & Environment cards. // Show loading state for both Facts & Environment cards.
toggleLoader('show'); toggleLoader('show');

View File

@ -136,7 +136,7 @@ function initFormElements() {
function moveOptionUp(element: HTMLSelectElement): void { function moveOptionUp(element: HTMLSelectElement): void {
const options = Array.from(element.options); const options = Array.from(element.options);
for (let i = 1; i < options.length; i++) { for (let i = 1; i < options.length; i++) {
let option = options[i]; const option = options[i];
if (option.selected) { if (option.selected) {
element.removeChild(option); element.removeChild(option);
element.insertBefore(option, element.options[i - 1]); element.insertBefore(option, element.options[i - 1]);
@ -290,7 +290,7 @@ function initScopeSelector() {
} }
} }
export function initForms() { export function initForms(): void {
for (const func of [ for (const func of [
initFormElements, initFormElements,
initFormActions, initFormActions,

View File

@ -33,6 +33,8 @@ interface Window {
*/ */
type Index<O extends Dict, K extends keyof O> = K extends string ? K : never; type Index<O extends Dict, K extends keyof O> = K extends string ? K : never;
type APIResponse<T> = T | ErrorBase | APIError;
type APIAnswer<T> = { type APIAnswer<T> = {
count: number; count: number;
next: Nullable<string>; next: Nullable<string>;

View File

@ -44,10 +44,13 @@ function updateLabel(status: JobStatus) {
switch (status.value) { switch (status.value) {
case 'failed' || 'errored': case 'failed' || 'errored':
labelClass = 'danger'; labelClass = 'danger';
break;
case 'running': case 'running':
labelClass = 'warning'; labelClass = 'warning';
break;
case 'completed': case 'completed':
labelClass = 'success'; labelClass = 'success';
break;
} }
element.setAttribute('class', `badge bg-${labelClass}`); element.setAttribute('class', `badge bg-${labelClass}`);
element.innerText = status.label; element.innerText = status.label;

View File

@ -3,7 +3,7 @@ import { isTruthy, getElements } from './util';
/** /**
* Allow any element to be made "clickable" with the use of the `data-href` attribute. * 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]')) { for (const link of getElements('*[data-href]')) {
const href = link.getAttribute('data-href'); const href = link.getAttribute('data-href');
if (isTruthy(href)) { if (isTruthy(href)) {

View File

@ -13,7 +13,7 @@ import { initSideNav } from './sidenav';
import { initRackElevation } from './racks'; import { initRackElevation } from './racks';
import { initLinks } from './links'; import { initLinks } from './links';
function initDocument() { function initDocument(): void {
for (const init of [ for (const init of [
initBootstrap, initBootstrap,
initColorMode, initColorMode,
@ -34,7 +34,7 @@ function initDocument() {
} }
} }
function initWindow() { function initWindow(): void {
const contentContainer = document.querySelector<HTMLElement>('.content-container'); const contentContainer = document.querySelector<HTMLElement>('.content-container');
if (contentContainer !== null) { if (contentContainer !== null) {
// Focus the content container for accessible navigation. // Focus the content container for accessible navigation.

View File

@ -67,7 +67,7 @@ function handleRackImageToggle(
* Add onClick callback for toggling rack elevation images. Synchronize the image toggle button * Add onClick callback for toggling rack elevation images. Synchronize the image toggle button
* text and display state of images with the local state. * text and display state of images with the local state.
*/ */
export function initRackElevation() { export function initRackElevation(): void {
const initiallyHidden = rackImagesState.get('hidden'); const initiallyHidden = rackImagesState.get('hidden');
for (const button of getElements<HTMLButtonElement>('button.toggle-images')) { for (const button of getElements<HTMLButtonElement>('button.toggle-images')) {
toggleRackImagesButton(initiallyHidden, button); toggleRackImagesButton(initiallyHidden, button);

View File

@ -8,7 +8,7 @@ import { getElements, getRowValues, findFirstAdjacent, isTruthy } from './util';
* @param event "click" event for each dropdown item. * @param event "click" event for each dropdown item.
* @param button Each dropdown item element. * @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 dropdown = event.currentTarget as HTMLButtonElement;
const selectedValue = findFirstAdjacent<HTMLSpanElement>(dropdown, 'span.search-obj-selected'); const selectedValue = findFirstAdjacent<HTMLSpanElement>(dropdown, 'span.search-obj-selected');
const selectedType = findFirstAdjacent<HTMLInputElement>(dropdown, 'input.search-obj-type'); const selectedType = findFirstAdjacent<HTMLInputElement>(dropdown, 'input.search-obj-type');
@ -31,7 +31,7 @@ function handleSearchDropdownClick(event: Event, button: HTMLButtonElement) {
/** /**
* Initialize Search Bar Elements. * Initialize Search Bar Elements.
*/ */
function initSearchBar() { function initSearchBar(): void {
for (const dropdown of getElements<HTMLUListElement>('.search-obj-selector')) { for (const dropdown of getElements<HTMLUListElement>('.search-obj-selector')) {
for (const button of dropdown.querySelectorAll<HTMLButtonElement>( for (const button of dropdown.querySelectorAll<HTMLButtonElement>(
'li > button.dropdown-item', 'li > button.dropdown-item',
@ -44,7 +44,7 @@ function initSearchBar() {
/** /**
* Initialize Interface Table Filter Elements. * Initialize Interface Table Filter Elements.
*/ */
function initInterfaceFilter() { function initInterfaceFilter(): void {
for (const input of getElements<HTMLInputElement>('input.interface-filter')) { for (const input of getElements<HTMLInputElement>('input.interface-filter')) {
const table = findFirstAdjacent<HTMLTableElement>(input, 'table'); const table = findFirstAdjacent<HTMLTableElement>(input, 'table');
const rows = Array.from( const rows = Array.from(
@ -53,7 +53,7 @@ function initInterfaceFilter() {
/** /**
* Filter on-page table by input text. * Filter on-page table by input text.
*/ */
function handleInput(event: Event) { function handleInput(event: Event): void {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
// Create a regex pattern from the input search text to match against. // Create a regex pattern from the input search text to match against.
const filter = new RegExp(target.value.toLowerCase().trim()); const filter = new RegExp(target.value.toLowerCase().trim());
@ -87,7 +87,7 @@ function initInterfaceFilter() {
} }
} }
function initTableFilter() { function initTableFilter(): void {
for (const input of getElements<HTMLInputElement>('input.object-filter')) { for (const input of getElements<HTMLInputElement>('input.object-filter')) {
// Find the first adjacent table element. // Find the first adjacent table element.
const table = findFirstAdjacent<HTMLTableElement>(input, 'table'); const table = findFirstAdjacent<HTMLTableElement>(input, 'table');
@ -101,7 +101,7 @@ function initTableFilter() {
* Filter table rows by matched input text. * Filter table rows by matched input text.
* @param event * @param event
*/ */
function handleInput(event: Event) { function handleInput(event: Event): void {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
// Create a regex pattern from the input search text to match against. // 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]) { for (const func of [initSearchBar, initTableFilter, initInterfaceFilter]) {
func(); func();
} }

View File

@ -711,7 +711,7 @@ class APISelect {
* @param id DOM ID of the other element. * @param id DOM ID of the other element.
*/ */
private updatePathValues(id: string): void { private updatePathValues(id: string): void {
let key = id.replaceAll(/^id_/gi, ''); const key = id.replaceAll(/^id_/gi, '');
const element = getElement<HTMLSelectElement>(`id_${key}`); const element = getElement<HTMLSelectElement>(`id_${key}`);
if (element !== null) { if (element !== null) {
// If this element's URL contains Django template tags ({{), replace the template tag // If this element's URL contains Django template tags ({{), replace the template tag
@ -982,15 +982,16 @@ class APISelect {
'button', 'button',
{ type: 'button' }, { type: 'button' },
['btn', 'btn-sm', 'btn-ghost-dark'], ['btn', 'btn-sm', 'btn-ghost-dark'],
[createElement('i', {}, ['mdi', 'mdi-reload'])], [createElement('i', null, ['mdi', 'mdi-reload'])],
); );
refreshButton.addEventListener('click', () => this.loadData()); refreshButton.addEventListener('click', () => this.loadData());
refreshButton.type = 'button';
this.slim.slim.search.container.appendChild(refreshButton); this.slim.slim.search.container.appendChild(refreshButton);
} }
} }
} }
export function initApiSelect() { export function initApiSelect(): void {
for (const select of getElements<HTMLSelectElement>('.netbox-api-select')) { for (const select of getElements<HTMLSelectElement>('.netbox-api-select')) {
new APISelect(select); new APISelect(select);
} }

View File

@ -11,6 +11,30 @@ function canChangeColor(option: Option | HTMLOptionElement): boolean {
return typeof option.value === 'string' && option.value !== ''; return typeof option.value === 'string' && option.value !== '';
} }
/**
* Style the container element based on the selected option value.
*/
function styleContainer(
instance: InstanceType<typeof SlimSelect>,
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 * Initialize color selection widget. Dynamically change the style of the select container to match
* the selected option. * the selected option.
@ -40,7 +64,7 @@ export function initColorSelect(): void {
// Style the select container to match any pre-selectd options. // Style the select container to match any pre-selectd options.
for (const option of instance.data.data) { for (const option of instance.data.data) {
if ('selected' in option && option.selected) { if ('selected' in option && option.selected) {
styleContainer(option); styleContainer(instance, option);
break; break;
} }
} }
@ -50,25 +74,7 @@ export function initColorSelect(): void {
instance.slim.container.classList.remove(className); 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. // Change the SlimSelect container's style based on the selected option.
instance.onChange = styleContainer; instance.onChange = option => styleContainer(instance, option);
} }
} }

View File

@ -2,7 +2,7 @@ import { initApiSelect } from './api';
import { initColorSelect } from './color'; import { initColorSelect } from './color';
import { initStaticSelect } from './static'; import { initStaticSelect } from './static';
export function initSelect() { export function initSelect(): void {
for (const func of [initApiSelect, initColorSelect, initStaticSelect]) { for (const func of [initApiSelect, initColorSelect, initStaticSelect]) {
func(); func();
} }

View File

@ -1,7 +1,7 @@
import SlimSelect from 'slim-select'; import SlimSelect from 'slim-select';
import { getElements } from '../util'; import { getElements } from '../util';
export function initStaticSelect() { export function initStaticSelect(): void {
for (const select of getElements<HTMLSelectElement>('.netbox-static-select')) { for (const select of getElements<HTMLSelectElement>('.netbox-static-select')) {
if (select !== null) { if (select !== null) {
const label = document.querySelector(`label[for=${select.id}]`) as HTMLLabelElement; const label = document.querySelector(`label[for=${select.id}]`) as HTMLLabelElement;

View File

@ -237,7 +237,7 @@ class SideNav {
'.navbar-nav .nav .nav-item a.nav-link', '.navbar-nav .nav .nav-item a.nav-link',
)) { )) {
const href = new RegExp(link.href, 'gi'); const href = new RegExp(link.href, 'gi');
if (Boolean(window.location.href.match(href))) { if (window.location.href.match(href)) {
yield link; yield link;
} }
} }
@ -310,7 +310,7 @@ class SideNav {
} }
} }
export function initSideNav() { export function initSideNav(): void {
for (const sidenav of getElements<HTMLDivElement>('.sidenav')) { for (const sidenav of getElements<HTMLDivElement>('.sidenav')) {
new SideNav(sidenav); new SideNav(sidenav);
} }

View File

@ -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 * Mark each option element in the selected columns element as 'selected' so they are submitted to
* the API. * the API.
*/ */
function saveTableConfig() { function saveTableConfig(): void {
for (const element of getElements<HTMLOptionElement>('select[name="columns"] option')) { for (const element of getElements<HTMLOptionElement>('select[name="columns"] option')) {
element.selected = true; element.selected = true;
} }
@ -14,7 +14,7 @@ function saveTableConfig() {
/** /**
* Delete all selected columns, which reverts the user's preferences to the default column set. * 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<HTMLSelectElement>('select[name="columns"]')) { for (const element of getElements<HTMLSelectElement>('select[name="columns"]')) {
element.value = ''; element.value = '';
} }
@ -23,7 +23,7 @@ function resetTableConfig() {
/** /**
* Add columns to the table config select element. * Add columns to the table config select element.
*/ */
function addColumns(event: Event) { function addColumns(event: Event): void {
for (const selectedOption of getElements<HTMLOptionElement>('#id_available_columns > option')) { for (const selectedOption of getElements<HTMLOptionElement>('#id_available_columns > option')) {
if (selectedOption.selected) { if (selectedOption.selected) {
for (const selected of getElements<HTMLSelectElement>('#id_columns')) { for (const selected of getElements<HTMLSelectElement>('#id_columns')) {
@ -38,7 +38,7 @@ function addColumns(event: Event) {
/** /**
* Remove columns from the table config select element. * Remove columns from the table config select element.
*/ */
function removeColumns(event: Event) { function removeColumns(event: Event): void {
for (const selectedOption of getElements<HTMLOptionElement>('#id_columns > option')) { for (const selectedOption of getElements<HTMLOptionElement>('#id_columns > option')) {
if (selectedOption.selected) { if (selectedOption.selected) {
for (const available of getElements<HTMLSelectElement>('#id_available_columns')) { for (const available of getElements<HTMLSelectElement>('#id_available_columns')) {
@ -53,7 +53,7 @@ function removeColumns(event: Event) {
/** /**
* Submit form configuration to the NetBox API. * Submit form configuration to the NetBox API.
*/ */
async function submitFormConfig(formConfig: Dict<Dict>) { async function submitFormConfig(formConfig: Dict<Dict>): Promise<APIResponse<APIUserConfig>> {
return await apiPatch<APIUserConfig>('/api/users/config/', formConfig); return await apiPatch<APIUserConfig>('/api/users/config/', formConfig);
} }
@ -61,7 +61,7 @@ async function submitFormConfig(formConfig: Dict<Dict>) {
* Handle table config form submission. Sends the selected columns to the NetBox API to update * Handle table config form submission. Sends the selected columns to the NetBox API to update
* the user's table configuration preferences. * the user's table configuration preferences.
*/ */
function handleSubmit(event: Event) { function handleSubmit(event: Event): void {
event.preventDefault(); event.preventDefault();
const element = event.currentTarget as HTMLFormElement; const element = event.currentTarget as HTMLFormElement;
@ -96,7 +96,7 @@ function handleSubmit(event: Event) {
/** /**
* Initialize table configuration elements. * Initialize table configuration elements.
*/ */
export function initTableConfig() { export function initTableConfig(): void {
for (const element of getElements<HTMLButtonElement>('#save_tableconfig')) { for (const element of getElements<HTMLButtonElement>('#save_tableconfig')) {
element.addEventListener('click', saveTableConfig); element.addEventListener('click', saveTableConfig);
} }

View File

@ -164,18 +164,14 @@ class TableState {
private table: HTMLTableElement; private table: HTMLTableElement;
/** /**
* Instance of ButtonState for the 'show/hide enabled rows' button. * 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; private enabledButton: ButtonState;
/** /**
* Instance of ButtonState for the 'show/hide disabled rows' button. * 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; private disabledButton: ButtonState;
/** /**
@ -288,7 +284,7 @@ class TableState {
/** /**
* Initialize table states. * Initialize table states.
*/ */
export function initInterfaceTable() { export function initInterfaceTable(): void {
for (const element of getElements<HTMLTableElement>('table')) { for (const element of getElements<HTMLTableElement>('table')) {
new TableState(element); new TableState(element);
} }

View File

@ -1,19 +1,18 @@
import Cookie from 'cookie'; import Cookie from 'cookie';
type APIRes<T> = T | ErrorBase | APIError;
type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; type Method = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
type ReqData = URLSearchParams | Dict | undefined | unknown; type ReqData = URLSearchParams | Dict | undefined | unknown;
type SelectedOption = { name: string; options: string[] }; type SelectedOption = { name: string; options: string[] };
type HTMLElementProperties<E extends HTMLElement> = /**
| { * Infer valid HTMLElement props based on element name.
[k in keyof E]: E[k]; */
} type InferredProps<
| {}; // Element name.
T extends keyof HTMLElementTagNameMap,
type InferredProps<T extends keyof HTMLElementTagNameMap> = HTMLElementProperties< // Element type.
HTMLElementTagNameMap[T] E extends HTMLElementTagNameMap[T] = HTMLElementTagNameMap[T]
>; > = Partial<Record<keyof E, E[keyof E]>>;
export function isApiError(data: Record<string, unknown>): data is APIError { export function isApiError(data: Record<string, unknown>): data is APIError {
return 'error' in data && 'exception' in data; return 'error' in data && 'exception' in data;
@ -36,9 +35,9 @@ export function hasMore(data: APIAnswer<APIObjectBase>): data is APIAnswerWithNe
*/ */
export function slugify(slug: string, chars: number): string { export function slugify(slug: string, chars: number): string {
return slug return slug
.replace(/[^\-\.\w\s]/g, '') // Remove unneeded chars .replace(/[^\-.\w\s]/g, '') // Remove unneeded chars
.replace(/^[\s\.]+|[\s\.]+$/g, '') // Trim leading/trailing spaces .replace(/^[\s.]+|[\s.]+$/g, '') // Trim leading/trailing spaces
.replace(/[\-\.\s]+/g, '-') // Convert spaces and decimals to hyphens .replace(/[-.\s]+/g, '-') // Convert spaces and decimals to hyphens
.toLowerCase() // Convert to lowercase .toLowerCase() // Convert to lowercase
.substring(0, chars); // Trim to first chars chars .substring(0, chars); // Trim to first chars chars
} }
@ -82,7 +81,7 @@ export async function apiRequest<R extends Dict, D extends ReqData = undefined>(
url: string, url: string,
method: Method, method: Method,
data?: D, data?: D,
): Promise<APIRes<R>> { ): Promise<APIResponse<R>> {
const token = getCsrfToken(); const token = getCsrfToken();
const headers = new Headers({ 'X-CSRFToken': token }); const headers = new Headers({ 'X-CSRFToken': token });
@ -111,18 +110,18 @@ export async function apiRequest<R extends Dict, D extends ReqData = undefined>(
export async function apiPatch<R extends Dict, D extends ReqData = Dict>( export async function apiPatch<R extends Dict, D extends ReqData = Dict>(
url: string, url: string,
data: D, data: D,
): Promise<APIRes<R>> { ): Promise<APIResponse<R>> {
return await apiRequest(url, 'PATCH', data); return await apiRequest(url, 'PATCH', data);
} }
export async function apiGetBase<R extends Dict>(url: string): Promise<APIRes<R>> { export async function apiGetBase<R extends Dict>(url: string): Promise<APIResponse<R>> {
return await apiRequest<R>(url, 'GET'); return await apiRequest<R>(url, 'GET');
} }
export async function apiPostForm<R extends Dict, D extends Dict>( export async function apiPostForm<R extends Dict, D extends Dict>(
url: string, url: string,
data: D, data: D,
): Promise<APIRes<R>> { ): Promise<APIResponse<R>> {
const body = new URLSearchParams(); const body = new URLSearchParams();
for (const [k, v] of Object.entries(data)) { for (const [k, v] of Object.entries(data)) {
body.append(k, String(v)); body.append(k, String(v));
@ -149,7 +148,7 @@ export function getElements<K extends keyof HTMLElementTagNameMap>(
export function getElements<E extends Element>(...key: string[]): Generator<E>; export function getElements<E extends Element>(...key: string[]): Generator<E>;
export function* getElements( export function* getElements(
...key: (string | keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap)[] ...key: (string | keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap)[]
) { ): Generator<Element> {
for (const query of key) { for (const query of key) {
for (const element of document.querySelectorAll(query)) { for (const element of document.querySelectorAll(query)) {
if (element !== null) { if (element !== null) {
@ -249,7 +248,7 @@ export function getNetboxData(key: string): string | null {
/** /**
* Toggle visibility of card loader. * Toggle visibility of card loader.
*/ */
export function toggleLoader(action: 'show' | 'hide') { export function toggleLoader(action: 'show' | 'hide'): void {
for (const element of getElements<HTMLDivElement>('div.card-overlay')) { for (const element of getElements<HTMLDivElement>('div.card-overlay')) {
if (action === 'show') { if (action === 'show') {
element.classList.remove('d-none'); element.classList.remove('d-none');
@ -316,25 +315,27 @@ export function findFirstAdjacent<R extends HTMLElement, B extends Element = Ele
* @param children Child elements. * @param children Child elements.
*/ */
export function createElement< export function createElement<
// Element name.
T extends keyof HTMLElementTagNameMap, T extends keyof HTMLElementTagNameMap,
// Element props.
P extends InferredProps<T>,
// Child element type.
C extends HTMLElement = HTMLElement C extends HTMLElement = HTMLElement
>( >(tag: T, properties: P | null, classes: string[], children: C[] = []): HTMLElementTagNameMap[T] {
tag: T,
properties: InferredProps<T>,
classes: string[],
children: C[] = [],
): HTMLElementTagNameMap[T] {
// Create the base element. // Create the base element.
const element = document.createElement<T>(tag); const element = document.createElement<T>(tag);
for (const k of Object.keys(properties)) { if (properties !== null) {
// Add each property to the element. for (const k of Object.keys(properties)) {
const key = k as keyof HTMLElementProperties<HTMLElementTagNameMap[T]>; // Add each property to the element.
const value = properties[key]; const key = k as keyof InferredProps<T>;
if (key in element) { const value = properties[key] as NonNullable<P[keyof P]>;
element[key] = value; if (key in element) {
element[key] = value;
}
} }
} }
// Add each CSS class to the element's class list. // Add each CSS class to the element's class list.
element.classList.add(...classes); element.classList.add(...classes);