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[] }; export function isApiError(data: Record): data is APIError { return 'error' in data && 'exception' in data; } export function hasError(data: Record): data is ErrorBase { return 'error' in data; } /** * Type guard to determine if a value is not null, undefined, or empty. */ export function isTruthy( value: V, ): value is NonNullable { const badStrings = ['', 'null', 'undefined']; if (typeof value === 'string' && !badStrings.includes(value)) { return true; } else if (typeof value === 'number') { return true; } else if (typeof value === 'boolean') { return true; } return false; } /** * Retrieve the CSRF token from cookie storage. */ export function getCsrfToken(): string { const { csrftoken: csrfToken } = Cookie.parse(document.cookie); if (typeof csrfToken === 'undefined') { throw new Error('Invalid or missing CSRF token'); } return csrfToken; } export async function apiRequest( url: string, method: Method, data?: D, ): Promise> { const token = getCsrfToken(); const headers = new Headers({ 'X-CSRFToken': token }); let body; if (typeof data !== 'undefined') { body = JSON.stringify(data); headers.set('content-type', 'application/json'); } const res = await fetch(url, { method, body, headers, credentials: 'same-origin' }); const contentType = res.headers.get('Content-Type'); if (typeof contentType === 'string' && contentType.includes('text')) { const error = await res.text(); return { error } as ErrorBase; } const json = (await res.json()) as R | APIError; if (!res.ok && Array.isArray(json)) { const error = json.join('\n'); return { error } as ErrorBase; } else if (!res.ok && 'detail' in json) { return { error: json.detail } as ErrorBase; } return json; } export async function apiPatch( url: string, data: D, ): Promise> { return await apiRequest(url, 'PATCH', data); } export async function apiGetBase(url: string): Promise> { return await apiRequest(url, 'GET'); } export async function apiPostForm( url: string, data: D, ): Promise> { const body = new URLSearchParams(); for (const [k, v] of Object.entries(data)) { body.append(k, String(v)); } return await apiRequest(url, 'POST', body); } /** * Fetch data from the NetBox API (authenticated). * @param url API endpoint */ export async function getApiData( url: string, ): Promise | ErrorBase | APIError> { return await apiGetBase>(url); } export function getElements( ...key: K[] ): Generator; export function getElements( ...key: K[] ): Generator; export function getElements(...key: string[]): Generator; export function* getElements( ...key: (string | keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap)[] ) { for (const query of key) { for (const element of document.querySelectorAll(query)) { if (element !== null) { yield element; } } } } /** * scrollTo() wrapper that calculates a Y offset relative to `element`, but also factors in an * offset relative to div#content-title. This ensures we scroll to the element, but leave enough * room to see said element. * * @param element Element to scroll to * @param offset Y Offset. 0 by default, to take into account the NetBox header. */ export function scrollTo(element: Element, offset: number = 0): void { let yOffset = offset; const title = document.getElementById('content-title') as Nullable; if (title !== null) { // If the #content-title element exists, add it to the offset. yOffset += title.getBoundingClientRect().bottom; } // Calculate the scrollTo target. const top = element.getBoundingClientRect().top + window.pageYOffset + yOffset; // Scroll to the calculated location. window.scrollTo({ top, behavior: 'smooth' }); return; } export function getSelectedOptions(base: E): SelectedOption[] { let selected = [] as SelectedOption[]; for (const element of base.querySelectorAll('select')) { if (element !== null) { const select = { name: element.name, options: [] } as SelectedOption; for (const option of element.options) { if (option.selected) { select.options.push(option.value); } } selected = [...selected, select]; } } return selected; }