import Cookie from 'cookie'; 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 apiGetBase>( url: string, ): Promise { const token = getCsrfToken(); const res = await fetch(url, { method: 'GET', headers: { 'X-CSRFToken': token }, 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 T | APIError; if (!res.ok && Array.isArray(json)) { const error = json.join('\n'); return { error } as ErrorBase; } return json; } export async function apiPostForm< T extends Record, R extends Record >(url: string, data: T): Promise { const token = getCsrfToken(); const body = new URLSearchParams(); for (const [k, v] of Object.entries(data)) { body.append(k, String(v)); } const res = await fetch(url, { method: 'POST', body, headers: { 'X-CSRFToken': token }, }); const contentType = res.headers.get('Content-Type'); if (typeof contentType === 'string' && contentType.includes('text')) { let error = await res.text(); if (contentType.includes('text/html')) { error = res.statusText; } return { error } as ErrorBase; } const json = (await res.json()) as R | APIError; if (!res.ok && 'detail' in json) { return { error: json.detail as string } as ErrorBase; } return json; } /** * 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; }