Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8c7760fc8 |
No files matched your search
+3
-1
@@ -68,7 +68,9 @@ All notable changes to Jarl are documented here. The format follows
|
||||
an explore page for it: answers over time, the top clients, names,
|
||||
views, types, results and blocklists among them (each a link that
|
||||
narrows it further), and the latest answers. "NXDOMAIN in external" →
|
||||
the clients asking → everything one of them asks. `GET /stats/explore`.
|
||||
the clients asking → everything one of them asks. A filter bar types
|
||||
filters in, and **Explore the answers** opens the page from the
|
||||
Statistics page. `GET /stats/explore`.
|
||||
- **Storage** on the dashboard: the size of Jarl's databases (and how many
|
||||
answers the query log holds), and per disk Jarl uses a bar of how full it
|
||||
is and how much of it the databases take. The Statistics page shows the
|
||||
|
||||
+5
-1
@@ -213,7 +213,11 @@ which shows, for what you clicked:
|
||||
* the latest 100 answers, every cell a link.
|
||||
|
||||
So "external, NXDOMAIN" leads to the clients asking, a client to everything
|
||||
it asks, one of its names to who else asks for it. Filters combine (this
|
||||
it asks, one of its names to who else asks for it. The page also opens
|
||||
from **Explore the answers** at the top of the Statistics page, and has a
|
||||
filter bar to type filters in: a client, a name (or it and the names below
|
||||
it), a view, a record type, a result, blocked or not; it suggests values
|
||||
from what is shown. Filters combine (this
|
||||
client, in this view, blocked only) and each has a ✕ to take it out. A name
|
||||
can be widened to the names below it (`domain=`). The Statistics page's
|
||||
view switches apply here too.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// last days, filtered by any mix of client, name, view, record type, result
|
||||
// and blocking. Every value is a link that adds it to the filter, so a
|
||||
// client leads to its names, a name to the clients asking for it, and so on.
|
||||
import { h, mount, fmtTime, errorBox, badge, backLink } from "../ui.js";
|
||||
import { h, mount, fmtTime, errorBox, badge, backLink, input, select, checkbox } from "../ui.js";
|
||||
import { get } from "../api.js";
|
||||
import { lineChart, chartCard, legend, timeTable, barList, compact } from "../charts.js";
|
||||
import { viewsOff } from "./stats.js";
|
||||
@@ -35,21 +35,18 @@ export async function render(el, params, query) {
|
||||
|
||||
// The Statistics page's view switches apply here too.
|
||||
const off = viewsOff();
|
||||
let views = "";
|
||||
if (off.size) {
|
||||
const all = (await get("/views").catch(() => [])).map((v) => v.name);
|
||||
views = all.filter((v) => !off.has(v)).join(",");
|
||||
}
|
||||
const allViews = (await get("/views").catch(() => [])).map((v) => v.name);
|
||||
const views = off.size ? allViews.filter((v) => !off.has(v)).join(",") : "";
|
||||
let ex;
|
||||
try {
|
||||
ex = await get("/stats/explore", { ...f, since, views: views || undefined });
|
||||
} catch (e) {
|
||||
mount(el, head(f, without, since, null), e.code === "query_log_off"
|
||||
mount(el, head(f, without, since, null), filterBar(f, since, null, allViews), e.code === "query_log_off"
|
||||
? h("div", { class: "msg info" }, e.message) : errorBox(e));
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = [head(f, without, since, ex)];
|
||||
const parts = [head(f, without, since, ex), filterBar(f, since, ex, allViews)];
|
||||
const maxSecs = retentionSecs(ex.retention);
|
||||
parts.push(h("div", { class: "filter-row", role: "group", "aria-label": "Time range" },
|
||||
h("span", { class: "muted" }, "Range"),
|
||||
@@ -86,6 +83,32 @@ export async function render(el, params, query) {
|
||||
mount(el, parts);
|
||||
}
|
||||
|
||||
// filterBar types filters in: a client, a name (or it and the names below
|
||||
// it), a view, a type, a result, blocked or not. It starts from the current
|
||||
// filters and suggests values from what is shown.
|
||||
function filterBar(f, since, ex, views) {
|
||||
const sugg = (id, entries) => h("datalist", { id }, (entries || []).map((e) => h("option", { value: e.key }, e.name || null)));
|
||||
const client = input({ value: f.client || "", placeholder: "address", class: "mono", list: "ex-clients", style: { width: "170px" } });
|
||||
const name = input({ value: f.name || f.domain || "", placeholder: "nas.adbraun.lol", class: "mono", list: "ex-names", style: { width: "220px" } });
|
||||
const below = checkbox("and below", !!f.domain);
|
||||
const view = select([["", "any view"], ...views.map((v) => [v, v])], f.view || "");
|
||||
const type = input({ value: f.type || "", placeholder: "any type", class: "mono", list: "ex-types", style: { width: "110px" } });
|
||||
const rcode = select([["", "any result"], "NOERROR", "NXDOMAIN", "SERVFAIL", "REFUSED"], f.rcode || "");
|
||||
const blocked = select([["", "blocked or not"], ["yes", "blocked"], ["no", "not blocked"], ...((ex && ex.by.blocked) || []).map((e) => [e.key, "blocked by " + blockedLabel(e.key)])], f.blocked || "");
|
||||
if (f.blocked && ![...blocked.options].some((o) => o.value === f.blocked)) blocked.append(h("option", { value: f.blocked, selected: true }, "blocked by " + blockedLabel(f.blocked)));
|
||||
const apply = () => {
|
||||
const n = name.value.trim().toLowerCase().replace(/\.$/, "");
|
||||
const g = { client: client.value.trim(), view: view.value, type: type.value.trim().toUpperCase(), rcode: rcode.value, blocked: blocked.value };
|
||||
if (n) g[below.input.checked ? "domain" : "name"] = n;
|
||||
location.hash = exploreHref(g, since);
|
||||
};
|
||||
const form = h("form", { class: "explore-filters", onsubmit: (e) => { e.preventDefault(); apply(); } },
|
||||
h("label", null, "Client ", client), h("label", null, "Name ", name), below, view, h("label", null, "Type ", type), rcode, blocked,
|
||||
h("button", { type: "submit", class: "primary small" }, "Show"),
|
||||
sugg("ex-clients", ex && ex.by.client), sugg("ex-names", ex && ex.by.name), sugg("ex-types", (ex && ex.by.type) || ["A", "AAAA", "PTR", "MX", "TXT", "HTTPS", "SRV"].map((k) => ({ key: k }))));
|
||||
return h("div", { class: "panel explore-filter-panel" }, form);
|
||||
}
|
||||
|
||||
// head is the title and the filter chips, each with a ✕ to take it out.
|
||||
function head(f, without, since, ex) {
|
||||
const chips = keys.filter((k) => f[k]).map((k) => h("span", { class: "chip filter-chip" },
|
||||
|
||||
@@ -43,7 +43,9 @@ export async function render(el, params, query) {
|
||||
h("div", { class: "muted" }, status.enabled
|
||||
? "From named's counters" + (status.query_log ? " and response log" : "") + " · data since " + (status.first_data ? fmtTime(status.first_data) : "–") +
|
||||
" · kept: minutes " + status.retention.minute + ", hours " + status.retention.hour + ", days " + status.retention.day
|
||||
: "Query statistics are off")));
|
||||
: "Query statistics are off")),
|
||||
status.enabled && status.retention && status.retention.queries
|
||||
? h("div", { class: "actions" }, h("a", { class: "button", href: "#/stats/explore", title: "Every answer of the last " + status.retention.queries + ", by client, name, view, type and result" }, "Explore the answers")) : null);
|
||||
if (!status.enabled) {
|
||||
mount(el, head, disabledPanel());
|
||||
return;
|
||||
|
||||
@@ -614,3 +614,8 @@ table.recent-queries td.oneline-cell { max-width: 40ch; white-space: nowrap; ove
|
||||
.swatch.db { background: var(--accent); }
|
||||
.swatch.other { background: var(--muted); opacity: 0.45; }
|
||||
.card .disk-bar { margin: 6px 0 4px; height: 8px; }
|
||||
|
||||
/* Explore: the filter bar. */
|
||||
form.explore-filters { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; }
|
||||
form.explore-filters label { color: var(--muted); display: inline-flex; gap: 6px; align-items: center; }
|
||||
.explore-filter-panel { padding-top: 10px; padding-bottom: 10px; }
|
||||
Reference in new issue
Block a user