Skip to content

Instantly share code, notes, and snippets.

@shokinn
Last active December 18, 2025 18:01
Show Gist options
  • Select an option

  • Save shokinn/8519012f44c012c568a3f6acc91c097c to your computer and use it in GitHub Desktop.

Select an option

Save shokinn/8519012f44c012c568a3f6acc91c097c to your computer and use it in GitHub Desktop.
Obsidian IPAM
```dataviewjs
const DEVICES_FOLDER = '"Permanent Notes/IPAM/Devices"';
const NETWORKS_FOLDER = '"Permanent Notes/IPAM/Networks"';
// ---------- helpers ----------
const norm = (s) => String(s ?? "").trim().toLowerCase().replace(/\s+/g, " ");
function isIpLikeToken(t) {
// digits/dots/slash only, and at least one dot
return /^[0-9./]+$/.test(t) && t.includes(".");
}
function isNumberToken(t) {
return /^[+-]?\d+(\.\d+)?$/.test(t);
}
// AND-search across tokens.
// IP-like & numeric tokens use strict substring match.
function rowMatchesQuery(query, haystack) {
const q = String(query ?? "").trim();
if (!q) return true;
const tokens = q.split(/\s+/).map(x => x.trim()).filter(Boolean);
const hay = norm(haystack);
return tokens.every(tok => {
const t = norm(tok);
if (!t) return true;
if (isIpLikeToken(t) || isNumberToken(t)) return hay.includes(t);
// default: substring match
return hay.includes(t);
});
}
function makeObsidianLink(file) {
const a = document.createElement("a");
a.setAttribute("data-href", file.path);
a.setAttribute("href", file.path);
a.classList.add("internal-link");
a.textContent = file.name;
return a;
}
// IPv4 -> unsigned 32-bit int
function ipv4ToU32(ip) {
if (!ip) return null;
const parts = String(ip).split(".").map(Number);
if (parts.length !== 4) return null;
if (parts.some(n => Number.isNaN(n) || n < 0 || n > 255)) return null;
return (((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0);
}
// IP or CIDR -> { ipNum, prefix }
function parseIpOrCidr(value) {
const s = String(value ?? "").trim();
if (!s) return null;
const [ipPart, prefixPart] = s.split("/");
const ipNum = ipv4ToU32(ipPart.trim());
if (ipNum === null) return null;
let prefix = null;
if (prefixPart != null && prefixPart.trim() !== "") {
const p = Number(prefixPart.trim());
if (!Number.isNaN(p)) prefix = p;
}
return { ipNum, prefix };
}
// Column typing: deterministic
const columnType = {
"VLAN ID": "num",
"IP address (CIDR)": "ip",
"Network address (CIDR)": "ip",
"Broadcast address": "ip",
"Gateway address": "ip",
"Subnetmask": "ip",
"Device": "text",
"Device type": "text",
"Network Device": "text",
"IP note": "text",
"IP configuration type": "text",
"Network name": "text",
};
function compareByType(a, b, type, asc = true) {
const dir = asc ? 1 : -1;
if (a == null) a = "";
if (b == null) b = "";
if (type === "num") {
const na = Number(String(a).trim());
const nb = Number(String(b).trim());
const aOk = !Number.isNaN(na);
const bOk = !Number.isNaN(nb);
if (aOk && bOk) return na < nb ? -1 * dir : na > nb ? 1 * dir : 0;
const sa = norm(a), sb = norm(b);
return sa < sb ? -1 * dir : sa > sb ? 1 * dir : 0;
}
if (type === "ip") {
const pa = parseIpOrCidr(a);
const pb = parseIpOrCidr(b);
if (pa && !pb) return -1 * dir;
if (!pa && pb) return 1 * dir;
if (!pa && !pb) {
const sa = norm(a), sb = norm(b);
return sa < sb ? -1 * dir : sa > sb ? 1 * dir : 0;
}
if (pa.ipNum < pb.ipNum) return -1 * dir;
if (pa.ipNum > pb.ipNum) return 1 * dir;
// prefix tiebreaker (nulls last)
const aHas = pa.prefix != null;
const bHas = pb.prefix != null;
if (aHas && !bHas) return -1 * dir;
if (!aHas && bHas) return 1 * dir;
if (aHas && bHas) {
if (pa.prefix < pb.prefix) return -1 * dir;
if (pa.prefix > pb.prefix) return 1 * dir;
}
return 0;
}
const sa = norm(a), sb = norm(b);
return sa < sb ? -1 * dir : sa > sb ? 1 * dir : 0;
}
// ---------- load data from Dataview index (FAST) ----------
const networks = dv.pages(NETWORKS_FOLDER)
.where(p => p.type === "network" && p.vlan_id != null);
const netByVlan = new Map(networks.map(n => [String(n.vlan_id), n]));
// devices with YAML list `ips`
const devices = dv.pages(DEVICES_FOLDER)
.where(p => (p.type === "ap" || p.type === "device" || p.type === "switch" || p.type === "router" || p.type === "ups") && Array.isArray(p.ips));
//.where(p => Array.isArray(p.ips));
let rows = [];
for (const d of devices) {
for (const e of d.ips) {
const vlan = String(e.vlan_id ?? "").trim();
const n = netByVlan.get(vlan) ?? null;
rows.push({
"VLAN ID": vlan,
"_deviceFile": d.file,
"Device type": String(d.type ?? "").trim(),
"IP address (CIDR)": String(e.ip ?? "").trim(),
"IP configuration type": String(e.type ?? "").trim(),
"Network Device": String(e.network_device_name ?? "").trim(),
"IP note": String(e.note ?? "").trim(),
"Network name": String(n?.network_name ?? ""),
"Network address (CIDR)": String(n?.network_cidr ?? ""),
"Broadcast address": String(n?.broadcast ?? ""),
"Gateway address": String(n?.gateway ?? ""),
"Subnetmask": String(n?.subnetmask ?? ""),
});
}
}
// ---------- UI ----------
const columns = [
"Device",
"Device type",
"Network Device",
"IP address (CIDR)",
"IP configuration type",
"IP note",
"VLAN ID",
"Network name",
"Network address (CIDR)",
"Broadcast address",
"Gateway address",
"Subnetmask",
];
let sortCol = "IP address (CIDR)";
let sortAsc = true;
const root = dv.el("div", "", { cls: "ipam-joined" });
const controls = root.createEl("div");
controls.style.display = "flex";
controls.style.gap = "0.75rem";
controls.style.alignItems = "center";
controls.style.marginBottom = "0.5rem";
controls.createEl("span", { text: "Search:" });
const search = controls.createEl("input", { type: "text", placeholder: "search…" });
search.style.flex = "1";
// Help / tooltip for search
const helpWrap = controls.createEl("span");
helpWrap.style.position = "relative";
helpWrap.style.display = "inline-flex";
helpWrap.style.alignItems = "center";
const helpIcon = helpWrap.createEl("span", { text: " ?" });
helpIcon.style.cursor = "help";
helpIcon.style.fontWeight = "bold";
helpIcon.style.opacity = "0.7";
helpIcon.style.userSelect = "none";
// Tooltip box
const helpBox = helpWrap.createEl("div");
helpBox.style.position = "absolute";
helpBox.style.top = "1.6em";
helpBox.style.right = "0";
helpBox.style.minWidth = "320px";
helpBox.style.maxWidth = "420px";
helpBox.style.padding = "10px 12px";
helpBox.style.borderRadius = "8px";
helpBox.style.boxShadow = "0 6px 20px rgba(0,0,0,0.25)";
helpBox.style.background = "var(--background-primary)";
helpBox.style.border = "1px solid var(--background-modifier-border)";
helpBox.style.fontSize = "0.85em";
helpBox.style.lineHeight = "1.4";
helpBox.style.display = "none";
helpBox.style.zIndex = "1000";
helpBox.innerHTML = `
<strong>Search help</strong><br><br>
• Search matches <em>all columns</em> by default<br>
• Multiple words are combined with <strong>AND</strong><br>
• IPs & numbers use exact matching (no fuzzy guessing)<br><br>
<strong>Restrict by column</strong><br>
Use the dropdown, or type shortcuts:<br>
<code>ip:192.168.1.1</code><br>
<code>vlan:10</code><br>
<code>net:servers</code><br>
<code>gw:192.168.10.1</code><br>
<code>mask:255.255.255.0</code><br><br>
<strong>Examples</strong><br>
<code>vlan:10 gateway</code><br>
<code>ip:192.168.10.1</code>
`;
// show / hide behavior
helpIcon.addEventListener("mouseenter", () => helpBox.style.display = "block");
helpIcon.addEventListener("mouseleave", () => helpBox.style.display = "none");
// allow click-to-toggle (useful on touch devices)
helpIcon.addEventListener("click", () => {
helpBox.style.display = helpBox.style.display === "none" ? "block" : "none";
});
// Column selector
const colSelect = controls.createEl("select");
colSelect.style.minWidth = "220px";
const optAll = colSelect.createEl("option", { text: "All columns" });
optAll.value = "__all__";
for (const col of columns) {
const opt = colSelect.createEl("option", { text: col });
opt.value = col;
}
const clearBtn = controls.createEl("button", { text: "Clear filters" });
clearBtn.style.whiteSpace = "nowrap";
const info = controls.createEl("span", { text: "" });
info.style.opacity = "0.75";
info.style.whiteSpace = "nowrap";
const tableWrap = root.createEl("div");
// --- per-column filter state ---
const filters = {}; // col -> { mode: "select"|"text", value: []|string }
// Choose filter control type per column
function filterModeFor(col) {
// checkbox-popup for low-cardinality columns
// if (["VLAN ID", "IP address (CIDR)", "Device", "Network Device", "Network name", "Network address (CIDR)", "Subnetmask"].includes(col)) return "select";
// return "text";
return "select";
}
// Build unique values for select filters
function uniqueValuesFor(col) {
const set = new Set();
for (const r of rows) {
let v = (col === "Device") ? (r["_deviceFile"]?.name ?? "") : (r[col] ?? "");
v = String(v ?? "").trim();
if (v) set.add(v);
}
const arr = Array.from(set);
arr.sort((a, b) => compareByType(a, b, columnType[col] ?? "text", true));
return arr;
}
// init filters
for (const col of columns) {
const mode = filterModeFor(col);
filters[col] = { mode, value: mode === "select" ? [] : "" };
}
// Optional: shortcut aliases
const aliases = {
vlan: "VLAN ID",
ip: "IP address (CIDR)",
dev: "Device",
device: "Device",
note: "IP note",
net: "Network name",
netaddr: "Network address (CIDR)",
gw: "Gateway address",
bc: "Broadcast address",
mask: "Subnetmask",
};
// Apply all filters to a row
function rowPassesColumnFilters(r) {
for (const col of columns) {
const f = filters[col];
let cell = (col === "Device") ? (r["_deviceFile"]?.name ?? "") : (r[col] ?? "");
cell = String(cell ?? "").trim();
if (f.mode === "select") {
// multi-select: empty = allow all
if (Array.isArray(f.value) && f.value.length > 0) {
if (!f.value.includes(cell)) return false;
}
} else {
if (String(f.value ?? "").trim() && !rowMatchesQuery(f.value, cell)) return false;
}
}
return true;
}
// ---------- checkbox popup implementation ----------
let activePopup = null;
function openCheckboxPopup(col, anchorEl) {
if (activePopup) {
activePopup.remove();
activePopup = null;
}
const values = uniqueValuesFor(col);
const selected = new Set(filters[col].value || []);
const popup = document.createElement("div");
popup.style.position = "fixed"; // fixed so it stays near the header even when scrolling
popup.style.zIndex = "5000";
popup.style.minWidth = "260px";
popup.style.maxHeight = "320px";
popup.style.overflow = "auto";
popup.style.padding = "10px";
popup.style.borderRadius = "10px";
popup.style.background = "var(--background-primary)";
popup.style.border = "1px solid var(--background-modifier-border)";
popup.style.boxShadow = "0 12px 30px rgba(0,0,0,0.35)";
// position under anchor
const rect = anchorEl.getBoundingClientRect();
popup.style.left = `${Math.min(rect.left, window.innerWidth - 280)}px`;
popup.style.top = `${rect.bottom + 6}px`;
// title + active count
const titleRow = popup.createEl("div");
titleRow.style.display = "flex";
titleRow.style.justifyContent = "space-between";
titleRow.style.alignItems = "center";
titleRow.style.gap = "10px";
titleRow.style.marginBottom = "8px";
titleRow.createEl("strong", { text: col });
const count = titleRow.createEl("span", { text: "" });
count.style.opacity = "0.75";
count.style.fontSize = "0.85em";
const searchBox = popup.createEl("input", { type: "text", placeholder: "Search values…" });
searchBox.style.width = "100%";
searchBox.style.marginBottom = "8px";
const btnRow = popup.createEl("div");
btnRow.style.display = "flex";
btnRow.style.gap = "6px";
btnRow.style.marginBottom = "8px";
const allBtn = btnRow.createEl("button", { text: "All" });
const noneBtn = btnRow.createEl("button", { text: "None" });
const closeBtn = btnRow.createEl("button", { text: "Close" });
closeBtn.style.marginLeft = "auto";
const list = popup.createEl("div");
function setFilterAndRerender() {
filters[col].value = Array.from(selected);
count.textContent = selected.size ? `${selected.size} selected` : "All";
render();
}
function renderList(filterText = "") {
list.empty();
const ft = norm(filterText);
for (const v of values) {
if (ft && !norm(v).includes(ft)) continue;
const row = list.createEl("label");
row.style.display = "flex";
row.style.alignItems = "center";
row.style.gap = "8px";
row.style.cursor = "pointer";
row.style.padding = "2px 0";
const cb = row.createEl("input", { type: "checkbox" });
cb.checked = selected.has(v);
cb.addEventListener("change", () => {
if (cb.checked) selected.add(v);
else selected.delete(v);
setFilterAndRerender();
});
row.createEl("span", { text: v });
}
}
searchBox.addEventListener("input", () => renderList(searchBox.value));
allBtn.addEventListener("click", () => {
values.forEach(v => selected.add(v));
setFilterAndRerender();
renderList(searchBox.value);
});
noneBtn.addEventListener("click", () => {
selected.clear(); // empty = All (no restriction)
setFilterAndRerender();
renderList(searchBox.value);
});
closeBtn.addEventListener("click", () => {
popup.remove();
activePopup = null;
});
document.body.appendChild(popup);
activePopup = popup;
setFilterAndRerender();
renderList();
// close when clicking outside
setTimeout(() => {
function onDocClick(e) {
if (!popup.contains(e.target)) {
popup.remove();
activePopup = null;
document.removeEventListener("click", onDocClick);
}
}
document.addEventListener("click", onDocClick);
}, 0);
}
function render() {
tableWrap.empty();
// parse optional key:value in the global search box
let rawQ = search.value ?? "";
let query = rawQ;
const m = String(rawQ).trim().match(/^(\w+)\s*:\s*(.+)$/);
if (m) {
const key = m[1].toLowerCase();
const val = m[2];
if (aliases[key]) {
colSelect.value = aliases[key];
query = val;
}
}
const selectedCol = colSelect.value;
let filtered = rows
.filter(rowPassesColumnFilters)
.filter(r => {
let hay;
if (selectedCol !== "__all__") {
if (selectedCol === "Device") hay = r["_deviceFile"]?.name ?? "";
else hay = r[selectedCol] ?? "";
} else {
hay = [
r["VLAN ID"],
r["IP address (CIDR)"],
r["_deviceFile"]?.name ?? "",
r["IP note"],
r["Network name"],
r["Network address (CIDR)"],
r["Broadcast address"],
r["Gateway address"],
r["Subnetmask"],
].join(" | ");
}
return rowMatchesQuery(query, hay);
});
// sorting
filtered.sort((a, b) => {
if (sortCol === "Device") {
return compareByType(a["_deviceFile"]?.name ?? "", b["_deviceFile"]?.name ?? "", "text", sortAsc);
}
const type = columnType[sortCol] ?? "text";
return compareByType(a[sortCol], b[sortCol], type, sortAsc);
});
info.textContent = `${filtered.length} / ${rows.length}`;
// table
const t = tableWrap.createEl("table");
t.className = "dataview table-view-table";
const thead = t.createEl("thead");
// header row (sortable)
const hr = thead.createEl("tr");
for (const col of columns) {
const th = hr.createEl("th");
th.style.cursor = "pointer";
th.textContent = col + (col === sortCol ? (sortAsc ? " ▲" : " ▼") : "");
th.onclick = () => {
if (sortCol === col) sortAsc = !sortAsc;
else { sortCol = col; sortAsc = true; }
render();
};
}
// filter row (text inputs + checkbox popup buttons)
const fr = thead.createEl("tr");
for (const col of columns) {
const th = fr.createEl("th");
th.style.paddingTop = "6px";
th.style.paddingBottom = "6px";
const f = filters[col];
if (f.mode === "select") {
const btn = th.createEl("button", { text: "Filter ▼" });
btn.style.width = "100%";
btn.style.cursor = "pointer";
btn.style.fontSize = "0.85em";
// show active state
const active = Array.isArray(f.value) && f.value.length > 0;
if (active) {
btn.textContent = `Filter (${f.value.length}) ▼`;
btn.style.fontWeight = "bold";
}
btn.addEventListener("click", (ev) => {
ev.stopPropagation();
openCheckboxPopup(col, btn);
});
} else {
const inp = th.createEl("input", { type: "text", placeholder: "filter…" });
inp.style.width = "100%";
inp.value = f.value;
inp.addEventListener("input", () => {
filters[col].value = inp.value;
render();
});
}
}
const tbody = t.createEl("tbody");
for (const r of filtered) {
const tr = tbody.createEl("tr");
for (const col of columns) {
const td = tr.createEl("td");
if (col === "Device") td.appendChild(makeObsidianLink(r["_deviceFile"]));
else td.setText(String(r[col] ?? ""));
}
}
}
// wiring
search.addEventListener("input", render);
colSelect.addEventListener("change", render);
clearBtn.addEventListener("click", () => {
search.value = "";
colSelect.value = "__all__";
for (const c of columns) {
filters[c].value = (filters[c].mode === "select") ? [] : "";
}
if (activePopup) { activePopup.remove(); activePopup = null; }
render();
});
render();
```
type ips
router
ip type vlan_id network_device_name note
192.168.1.1/24
static
1
br1_vlan1_default
Default Gateway
ip type vlan_id network_device_name note
192.168.8.1/24
static
8
br1_vlan8_ios
Default Gateway
ip type vlan_id network_device_name note
192.168.9.1/24
static
9
br1_vlan9_service
Default Gateway
ip type vlan_id network_device_name note
192.168.10.1/24
static
10
br1_vlan10_media
Default Gateway
ip type vlan_id network_device_name note
192.168.23.1/24
static
23
br1_vlan23_guest
Default Gateway
ip type vlan_id network_device_name note
192.168.42.1/24
static
42
br1_vlan42_lan
Default Gateway

nbx0router00

br1_vlan1_default

  • IP: 192.168.1.1/24
  • Type: Static
  • vlan id: 1
  • Note: Default Gateway

br1_vlan8_ios

  • IP: 192.168.8.1/24
  • Type: Static
  • vlan id: 8
  • Note: Default Gateway

br1_vlan9_service

  • IP: 192.168.9.1/24
  • Type: Static
  • vlan id: 9
  • Note: Default Gateway

br1_vlan10_media

  • IP: 192.168.10.1/24
  • Type: Static
  • vlan id: 10
  • Note: Default Gateway

br1_vlan23_guest

  • IP: 192.168.23.1/24
  • Type: Static
  • vlan id: 23
  • Note: Default Gateway

br1_vlan42_lan

  • IP: 192.168.42.1/24
  • Type: Static
  • vlan id: 42
  • Note: Default Gateway
type vlan_id network_name network_cidr broadcast gateway subnetmask note
network
1
default vlan
192.168.1.0/24
192.168.1.255
192.168.1.1
255.255.255.0
Default vlan, not in use

VLAN 1 — default vlan

  • Network (CIDR): 192.168.1.0/24
  • Broadcast: 192.168.1.255
  • Gateway: 192.168.1.1
  • Subnetmask: 255.255.1.0

Notes

Default vlan, not in use.

@shokinn
Copy link
Author

shokinn commented Dec 13, 2025

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment