// The dashboard: interfaces, subnets and how full they are, problems, // recent activity. import { h, mount, problemsList, table, time, plural } from "../ui.js"; import { get, can } from "../api.js"; import { usageBar, subnetLabel } from "../widgets.js"; import { onLeave } from "../app.js"; export async function render(el) { const draw = async () => { const st = await get("/status"); const events = can("leases") ? await get("/events", { limit: 8 }) : []; const leased = st.leases.active || 0; const reservations = st.subnets.reduce((n, s) => n + s.usage.reservations, 0); const poolSize = st.subnets.reduce((n, s) => n + (s.disabled ? 0 : s.usage.pool_size), 0); const notServing = st.interfaces.filter((i) => !i.listening); const errors = st.problems.filter((p) => p.severity === "error"); mount(el, h("div", { class: "page-head" }, h("div", { class: "grow" }, h("h1", null, "Dashboard"))), h("div", { class: "cards" }, card("Subnets", st.subnets.length, plural(st.interfaces.length, "interface"), "#/subnets"), card("Active leases", leased, poolSize ? Math.round(leased * 100 / poolSize) + "% of " + poolSize + " pool addresses" : "no pools yet", "#/leases"), card("Reservations", reservations, "fixed addresses", "#/reservations"), card("Problems", st.problems.length, errors.length ? plural(errors.length, "error") : notServing.length ? plural(notServing.length, "interface") + " not serving" : "none", null)), st.subnets.length === 0 ? h("div", { class: "panel" }, h("h2", null, "Getting started"), h("p", null, "Steward serves no subnets yet. Add one for each network, on the interface its clients are on: ", h("a", { href: "#/subnets?new=1" }, "New subnet"), "."), h("p", null, "Moving from Kea? On the server, ", h("code", null, "sudo steward import kea /etc/kea/kea-dhcp4.conf --leases /var/lib/kea/kea-leases4.csv"), " brings over the subnets, reservations, options and current leases.")) : null, h("div", { class: "panel" }, h("h2", null, "Interfaces"), st.interfaces.length ? table([ { label: "Interface", tdClass: "nowrap", render: (i) => [h("span", { class: "dot " + (i.listening ? "ok" : "err") }), h("span", { class: "mono" }, h("strong", null, i.name))] }, { label: "Addresses", render: (i) => h("span", { class: "mono small" }, i.addresses.join(", ") || "none") }, { label: "DHCP", render: (i) => i.listening ? h("span", { class: "muted" }, "serving") : h("span", { class: "err-text small" }, "not serving: " + i.error) }, { label: "Subnets", render: (i) => i.subnets.map((id, n) => { const s = st.subnets.find((x) => x.id === id); return [n ? ", " : "", s ? h("a", { href: "#/subnets/" + id }, subnetLabel(s)) : "#" + id]; }) }, ], st.interfaces) : h("div", { class: "muted" }, "DHCP is served on the interfaces that have subnets.")), st.subnets.length ? h("div", { class: "panel" }, h("h2", null, "Subnets"), table([ { label: "Subnet", render: (s) => h("a", { href: "#/subnets/" + s.id }, subnetLabel(s)) }, { label: "Interface", render: (s) => h("span", { class: "mono" }, s.interface) }, { label: "Pools in use", render: (s) => s.disabled ? h("span", { class: "badge off" }, "disabled") : usageBar(s.usage) }, { label: "Reservations", render: (s) => s.usage.reservations ? s.usage.reservations_leased + " of " + s.usage.reservations + " online" : "–" }, ], st.subnets)) : null, st.problems.length || st.ping_problem ? h("div", { class: "panel" }, h("h2", null, "Problems"), problemsList(st.problems), st.ping_problem ? h("div", { class: "msg warn" }, st.ping_problem) : null) : null, events.length ? h("div", { class: "panel" }, h("div", { class: "panel-head" }, h("h2", null, "Latest DHCP messages"), h("a", { class: "button small", href: "#/activity" }, "All activity")), table([ { label: "When", render: (e) => time(e.time) }, { label: "Interface", render: (e) => e.interface }, { label: "Message", render: (e) => h("span", { class: "type-" + e.type.replace(/^DHCP/, "").split(" ")[0] }, e.type.replace(/^DHCP/, "")) }, { label: "Client", render: (e) => h("span", { class: "mono" }, e.mac || "") }, { label: "Address", render: (e) => h("span", { class: "mono" }, e.ip || "") }, { label: "Host name", render: (e) => e.hostname || "" }, ], events, { })) : null); }; await draw(); const t = setInterval(() => { draw().catch(() => {}); }, 15000); onLeave(() => clearInterval(t)); } function card(label, value, sub, href) { const body = [h("div", { class: "label" }, label), h("div", { class: "value" }, String(value)), h("div", { class: "sub" }, sub)]; return href ? h("a", { class: "card link", href }, body) : h("div", { class: "card" }, body); }