From: Giorgio Ravera Date: Tue, 26 May 2026 16:55:26 +0000 (+0200) Subject: Added api.js & improved sort without re-fetch data X-Git-Url: http://git.giorgioravera.it/?a=commitdiff_plain;h=d1a1bd7ea13b493379035a5b282dc27768120e8a;p=network-manager.git Added api.js & improved sort without re-fetch data --- diff --git a/backend/app.py b/backend/app.py index 46b0010..cf96135 100644 --- a/backend/app.py +++ b/backend/app.py @@ -188,6 +188,10 @@ def css_layout(request: Request): def js_common(request: Request): return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/common.js")) +# JS API +def js_api(request: Request): + return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/api.js")) + # JS Services def js_services(request: Request): return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/services.js")) @@ -249,6 +253,7 @@ def create_app() -> FastAPI: app.add_api_route("/css/variables.css", css_variables, methods=["GET"]) app.add_api_route("/css/layout.css", css_layout, methods=["GET"]) app.add_api_route("/js/common.js", js_common, methods=["GET"]) + app.add_api_route("/js/api.js", js_api, methods=["GET"]) app.add_api_route("/js/services.js", js_services, methods=["GET"]) app.add_api_route("/favicon.ico", favicon, methods=["GET"]) diff --git a/frontend/js/aliases.js b/frontend/js/aliases.js index dc8887b..b0c657e 100644 --- a/frontend/js/aliases.js +++ b/frontend/js/aliases.js @@ -1,18 +1,20 @@ // Import common js import { loadModals, showToast, sortTable, initSortableTable, resetSorting } from './common.js'; import { reloadDNS, reloadDHCP } from './services.js'; +import { apiMap, fetchData } from './api.js'; // ----------------------------- // State variables // ----------------------------- +let allAliases = []; +let viewAliases = []; let editingAliasId = null; const sortState = { sortDirection: {}, lastSort: null }; // ----------------------------- // Load all aliases into the table // ----------------------------- -async function loadAliases() { - let aliases = []; +async function loadAliases(refresh = true) { const loader = document.getElementById("loader"); const container = document.getElementById("devices-container"); const dataTable = document.getElementById("dataTable"); @@ -20,49 +22,23 @@ async function loadAliases() { // hide table during loading to avoid flickering and show loader dataTable.classList.add("d-none"); - try { - // Show loader - loader.style.display = "block"; - - // Fetch data - const res = await fetch(`/api/aliases`, { - headers: { Accept: 'application/json' }, - }); - - // Check content-type to avoid parsing errors - const contentType = res.headers.get("content-type") || ""; - if (!contentType.includes("application/json")) { - const err = new Error(`${res.status}: ${res.statusText}`); - err.status = res.status; - throw err; - } - - // Check JSON - let data; + if(refresh) { try { - data = await res.json(); - aliases = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []); + // Show loader + loader.style.display = "block"; - } catch { - throw new Error('Invalid JSON payload'); - } + // Fetch devices + allAliases = await fetchData(apiMap.aliases); + viewAliases = [...allAliases]; - // Check JSON errors - if (!res.ok) { - const serverMsg = data?.detail?.message?.trim(); - const base = `Error loading aliases`; - const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); - err.status = res.status; - throw err; + } catch (err) { + console.error(err?.message || "Error loading aliases"); + showToast(err?.message || "Error loading aliase", false); + allAliases = []; + // hide loader and show table + loader.style.display = "none"; + dataTable.classList.remove("d-none"); } - - } catch (err) { - console.error(err?.message || "Error loading aliases"); - showToast(err?.message || "Error loading aliase", false); - aliases = []; - // hide loader and show table - loader.style.display = "none"; - dataTable.classList.remove("d-none"); } // DOM Reference @@ -76,7 +52,7 @@ async function loadAliases() { tbody.innerHTML = ""; // if no aliases, show an empty row - if (!aliases.length) { + if (!allAliases.length) { const trEmpty = document.createElement("tr"); const tdEmpty = document.createElement("td"); tdEmpty.colSpan = 7; @@ -93,7 +69,7 @@ async function loadAliases() { // fragment per performance const frag = document.createDocumentFragment(); - aliases.forEach(h => { + allAliases.forEach(h => { const id = Number(h.id); const tr = document.createElement("tr"); @@ -542,9 +518,12 @@ function filterAliases() { // ----------------------------- async function clearSearch() { const input = document.getElementById("searchInput"); - input.value = ""; - input.blur(); - await loadAliases(); + if (input) { + input.value = ""; + input.blur(); + } + viewAliases = [...allAliases]; + await loadAliases(false); } // ----------------------------- @@ -642,10 +621,10 @@ function initSearch() { // Escape management when focus is in the input input.addEventListener("keydown", (e) => { if (e.key === "Escape") { - e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) - e.stopPropagation(); // evita che arrivi al listener globale + e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) + e.stopPropagation(); // evita che arrivi al listener globale resetSorting(sortState); - clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) + clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) filterAliases(''); // ripristina tabella } }); diff --git a/frontend/js/api.js b/frontend/js/api.js new file mode 100644 index 0000000..0b6c4d2 --- /dev/null +++ b/frontend/js/api.js @@ -0,0 +1,60 @@ +// ----------------------------- +// API Endpoints +// ----------------------------- +export const apiMap = { + hosts: { + url: "/api/hosts", + name: "Hosts" + }, + aliases: { + url: "/api/aliases", + name: "Aliases" + }, + leases: { + url: "/api/dhcp/leases", + name: "Leases" + }, + devices: { + url: "/api/devices", + name: "Devices" + } +}; + +// ----------------------------- +// Fetch Data functions +// ----------------------------- +export async function fetchData(api) { + let items = []; + + // Fetch data + const res = await fetch(api.url, { + headers: { Accept: 'application/json' }, + }); + + // Check content-type to avoid parsing errors + const contentType = res.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + const err = new Error(`${res.status}: ${res.statusText}`); + err.status = res.status; + throw err; + } + + // Check JSON + let data; + try { + data = await res.json(); + items = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []); + } catch { + throw new Error('Invalid JSON payload'); + } + + // Check JSON errors + if (!res.ok) { + const serverMsg = data?.detail?.message?.trim(); + const base = `Error loading ${api.name}`; + const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); + err.status = res.status; + throw err; + } + return items; +} \ No newline at end of file diff --git a/frontend/js/devices.js b/frontend/js/devices.js index 72224c3..78db81f 100644 --- a/frontend/js/devices.js +++ b/frontend/js/devices.js @@ -1,18 +1,20 @@ // Import common js import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting } from './common.js'; import { reloadDNS, reloadDHCP } from './services.js'; +import { apiMap, fetchData } from './api.js'; // ----------------------------- // State variables // ----------------------------- +let allDevices = []; +let viewDevices = []; let editingHostId = null; const sortState = { sortDirection: {}, lastSort: null }; // ----------------------------- // Load all devices into the table // ----------------------------- -async function loadDevices() { - let devices = []; +async function loadDevices(refresh = true) { const loader = document.getElementById("loader"); const container = document.getElementById("devices-container"); const dataTable = document.getElementById("dataTable"); @@ -20,49 +22,23 @@ async function loadDevices() { // hide table during loading to avoid flickering and show loader dataTable.classList.add("d-none"); - try { - // Show loader - loader.style.display = "block"; - - // Fetch data - const res = await fetch(`/api/devices`, { - headers: { Accept: 'application/json' }, - }); - - // Check content-type to avoid parsing errors - const contentType = res.headers.get("content-type") || ""; - if (!contentType.includes("application/json")) { - const err = new Error(`${res.status}: ${res.statusText}`); - err.status = res.status; - throw err; - } - - // Check JSON - let data; + if(refresh) { try { - data = await res.json(); - devices = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []); + // Show loader + loader.style.display = "block"; - } catch { - throw new Error('Invalid JSON payload'); - } + // Fetch devices + allDevices = await fetchData(apiMap.devices); + viewDevices = [...allDevices]; - // Check JSON errors - if (!res.ok) { - const serverMsg = data?.detail?.message?.trim(); - const base = `Error loading devices`; - const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); - err.status = res.status; - throw err; + } catch (err) { + console.error(err?.message || "Error loading devices"); + showToast(err?.message || "Error loading devices", false); + allDevices = []; + // hide loader and show table + loader.style.display = "none"; + dataTable.classList.remove("d-none"); } - - } catch (err) { - console.error(err?.message || "Error loading devices"); - showToast(err?.message || "Error loading devices", false); - devices = []; - // hide loader and show table - loader.style.display = "none"; - dataTable.classList.remove("d-none"); } // DOM Reference @@ -76,7 +52,7 @@ async function loadDevices() { tbody.innerHTML = ""; // if no devices, show an empty row - if (!devices.length) { + if (!allDevices.length) { const trEmpty = document.createElement("tr"); const tdEmpty = document.createElement("td"); tdEmpty.colSpan = 7; @@ -93,7 +69,7 @@ async function loadDevices() { // fragment per performance const frag = document.createDocumentFragment(); - devices.forEach(d => { + allDevices.forEach(d => { //const mixedId = d.id; //const id = mixedId.slice(2); @@ -659,9 +635,12 @@ function filterDevices() { // ----------------------------- async function clearSearch() { const input = document.getElementById("searchInput"); - input.value = ""; - input.blur(); - await loadDevices(); + if (input) { + input.value = ""; + input.blur(); + } + viewDevices = [...allDevices]; + await loadDevices(false); } // ----------------------------- diff --git a/frontend/js/hosts.js b/frontend/js/hosts.js index 93ff0fc..2efdd77 100644 --- a/frontend/js/hosts.js +++ b/frontend/js/hosts.js @@ -1,18 +1,20 @@ // Import common js import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting } from './common.js'; import { reloadDNS, reloadDHCP } from './services.js'; +import { apiMap, fetchData } from './api.js'; // ----------------------------- // State variables // ----------------------------- +let allHosts = []; +let viewHosts = []; let editingHostId = null; const sortState = { sortDirection: {}, lastSort: null }; // ----------------------------- // Load all hosts into the table // ----------------------------- -async function loadHosts() { - let hosts = []; +async function loadHosts(refresh = true) { const loader = document.getElementById("loader"); const container = document.getElementById("devices-container"); const dataTable = document.getElementById("dataTable"); @@ -20,49 +22,23 @@ async function loadHosts() { // hide table during loading to avoid flickering and show loader dataTable.classList.add("d-none"); - try { - // Show loader - loader.style.display = "block"; - - // Fetch data - const res = await fetch(`/api/hosts`, { - headers: { Accept: 'application/json' }, - }); - - // Check content-type to avoid parsing errors - const contentType = res.headers.get("content-type") || ""; - if (!contentType.includes("application/json")) { - const err = new Error(`${res.status}: ${res.statusText}`); - err.status = res.status; - throw err; - } - - // Check JSON - let data; + if(refresh) { try { - data = await res.json(); - hosts = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []); + // Show loader + loader.style.display = "block"; - } catch { - throw new Error('Invalid JSON payload'); - } + // Fetch hosts + allHosts = await fetchData(apiMap.hosts); + viewHosts = [...allHosts]; - // Check JSON errors - if (!res.ok) { - const serverMsg = data?.detail?.message?.trim(); - const base = `Error loading hosts`; - const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); - err.status = res.status; - throw err; + } catch (err) { + console.error(err?.message || "Error loading hosts"); + showToast(err?.message || "Error loading hosts", false); + allHosts = []; + // hide loader and show table + loader.style.display = "none"; + dataTable.classList.remove("d-none"); } - - } catch (err) { - console.error(err?.message || "Error loading hosts"); - showToast(err?.message || "Error loading hosts", false); - hosts = []; - // hide loader and show table - loader.style.display = "none"; - dataTable.classList.remove("d-none"); } // DOM Reference @@ -76,7 +52,7 @@ async function loadHosts() { tbody.innerHTML = ""; // if no hosts, show an empty row - if (!hosts.length) { + if (!allHosts.length) { const trEmpty = document.createElement("tr"); const tdEmpty = document.createElement("td"); tdEmpty.colSpan = 7; @@ -93,7 +69,7 @@ async function loadHosts() { // fragment per performance const frag = document.createDocumentFragment(); - hosts.forEach(h => { + allHosts.forEach(h => { const id = Number(h.id); const tr = document.createElement("tr"); @@ -566,9 +542,12 @@ function filterHosts() { // ----------------------------- async function clearSearch() { const input = document.getElementById("searchInput"); - input.value = ""; - input.blur(); - await loadHosts(); + if (input) { + input.value = ""; + input.blur(); + } + viewHosts = [...allHosts]; + await loadHosts(false); } // ----------------------------- @@ -666,11 +645,11 @@ function initSearch() { // Escape management when focus is in the input input.addEventListener("keydown", (e) => { if (e.key === "Escape") { - e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) - e.stopPropagation(); // evita che arrivi al listener globale + e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) + e.stopPropagation(); // evita che arrivi al listener globale resetSorting(sortState); - clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) - filterHosts(''); // ripristina tabella + clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) + filterHosts(''); // ripristina tabella } }); } diff --git a/frontend/js/leases.js b/frontend/js/leases.js index 289f691..dd2db98 100644 --- a/frontend/js/leases.js +++ b/frontend/js/leases.js @@ -1,17 +1,19 @@ // Import common js import { loadModals, isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting } from './common.js'; import { reloadDNS, reloadDHCP } from './services.js'; +import { apiMap, fetchData } from './api.js'; // ----------------------------- // State variables // ----------------------------- +let allLeases = []; +let viewLeases = []; const sortState = { sortDirection: {}, lastSort: null }; // ----------------------------- // Load all leases into the table // ----------------------------- -async function loadLeases() { - let leases = []; +async function loadLeases(refresh = true) { const loader = document.getElementById("loader"); const container = document.getElementById("devices-container"); const dataTable = document.getElementById("dataTable"); @@ -19,49 +21,23 @@ async function loadLeases() { // hide table during loading to avoid flickering and show loader dataTable.classList.add("d-none"); - try { - // Show loader - loader.style.display = "block"; - - // Fetch data - const res = await fetch(`/api/dhcp/leases`, { - headers: { Accept: 'application/json' }, - }); - - // Check content-type to avoid parsing errors - const contentType = res.headers.get("content-type") || ""; - if (!contentType.includes("application/json")) { - const err = new Error(`${res.status}: ${res.statusText}`); - err.status = res.status; - throw err; - } - - // Check JSON - let data; + if(refresh) { try { - data = await res.json(); - leases = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []); + // Show loader + loader.style.display = "block"; - } catch { - throw new Error('Invalid JSON payload'); - } + // Fetch leases + allLeases = await fetchData(apiMap.leases); + viewLeases = [...allLeases]; - // Check JSON errors - if (!res.ok) { - const serverMsg = data?.detail?.message?.trim(); - const base = `Error loading leases`; - const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base); - err.status = res.status; - throw err; + } catch (err) { + console.error(err?.message || "Error loading leases"); + showToast(err?.message || "Error loading leases", false); + allLeases = []; + // hide loader and show table + loader.style.display = "none"; + dataTable.classList.remove("d-none"); } - - } catch (err) { - console.error(err?.message || "Error loading leases"); - showToast(err?.message || "Error loading leases", false); - leases = []; - // hide loader and show table - loader.style.display = "none"; - dataTable.classList.remove("d-none"); } // DOM Reference @@ -75,7 +51,7 @@ async function loadLeases() { tbody.innerHTML = ""; // if no leases, show an empty row - if (!leases.length) { + if (!allLeases.length) { const trEmpty = document.createElement("tr"); const tdEmpty = document.createElement("td"); tdEmpty.colSpan = 7; @@ -92,7 +68,7 @@ async function loadLeases() { // fragment per performance const frag = document.createDocumentFragment(); - leases.forEach(l => { + allLeases.forEach(l => { const id = Number(l.id); const tr = document.createElement("tr"); @@ -530,9 +506,12 @@ function filterLeases() { // ----------------------------- async function clearSearch() { const input = document.getElementById("searchInput"); - input.value = ""; - input.blur(); - await loadLeases(); + if (input) { + input.value = ""; + input.blur(); + } + viewLeases = [...allLeases]; + await loadLeases(false); } // ----------------------------- @@ -630,11 +609,11 @@ function initSearch() { // Escape management when focus is in the input input.addEventListener("keydown", (e) => { if (e.key === "Escape") { - e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) - e.stopPropagation(); // evita che arrivi al listener globale + e.preventDefault(); // evita side-effect (es. chiusure di modali del browser) + e.stopPropagation(); // evita che arrivi al listener globale resetSorting(sortState); - clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) - filterLeases(''); // ripristina tabella + clearSearch(); // svuota input e ricarica tabella (come definito nella tua funzione) + filterLeases(''); // ripristina tabella } }); } @@ -738,7 +717,7 @@ function handleKeyboard(e) { // Button event delegation (Enter, Space) const isEnter = e.key === 'Enter'; - const isSpace = e.key === ' ' || e.key === 'Spacebar'; + const isSpace = e.key === ' '; if (!isEnter && !isSpace) return; const el = e.target.closest('[data-action]');