Skip to content

Commit c0177a3

Browse files
committed
add filters to url search params
1 parent 585e05f commit c0177a3

File tree

4 files changed

+233
-7
lines changed

4 files changed

+233
-7
lines changed

lib/datadistributor.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ export interface Filter {
3232
run(data: any): Boolean;
3333
}
3434

35+
export interface GenericFilter extends Filter {
36+
getNegate(): boolean;
37+
getName(): string;
38+
getValue(): string;
39+
setNegate(negate: boolean): void;
40+
}
41+
3542
export type FilterMethod = (node: Node) => boolean;
3643

3744
export const DataDistributor = function () {

lib/filters/genericnode.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import * as helper from "../utils/helper.js";
2-
import { Filter } from "../datadistributor.js";
2+
import { GenericFilter } from "../datadistributor.js";
33
import { CanRender } from "../container.js";
44
import { Node } from "../utils/node.js";
5+
import { _ } from "../utils/language.js";
56

67
export const GenericNodeFilter = function (
78
name: string,
89
keys: string[],
910
value: string,
1011
nodeValueModifier: (a: any) => string,
11-
): Filter & CanRender {
12+
): GenericFilter & CanRender {
1213
let negate = false;
1314
let refresh: () => any;
1415

1516
let label = document.createElement("label");
1617
let strong = document.createElement("strong");
17-
label.textContent = name + ": ";
18+
label.textContent = _.t(name) + ": ";
1819
label.appendChild(strong);
1920

2021
function run(node: Node) {
@@ -56,14 +57,34 @@ export const GenericNodeFilter = function (
5657
};
5758
}
5859

60+
function setNegate(n: boolean) {
61+
negate = n;
62+
}
63+
5964
function getKey() {
6065
return value.concat(name);
6166
}
6267

68+
function getName() {
69+
return name;
70+
}
71+
72+
function getValue() {
73+
return value;
74+
}
75+
76+
function getNegate() {
77+
return negate;
78+
}
79+
6380
return {
6481
run,
6582
setRefresh,
6683
render,
6784
getKey,
85+
getNegate,
86+
getName,
87+
getValue,
88+
setNegate,
6889
};
6990
};

lib/proportions.ts

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as d3Interpolate from "d3-interpolate";
22
import { Moment } from "moment";
33
import { classModule, eventListenersModule, h, init, propsModule, styleModule, VNode } from "snabbdom";
4-
import { DataDistributor, Filter, ObjectsLinksAndNodes } from "./datadistributor.js";
4+
import { DataDistributor, Filter, GenericFilter, ObjectsLinksAndNodes } from "./datadistributor.js";
55
import { GenericNodeFilter } from "./filters/genericnode.js";
66
import * as helper from "./utils/helper.js";
77
import { _ } from "./utils/language.js";
@@ -13,6 +13,81 @@ type TableNode = {
1313
vnode?: VNode;
1414
};
1515

16+
const statusFieldMapping = {
17+
"node.status": {
18+
keys: ["is_online"],
19+
modifier: function (d: any) {
20+
return d ? "online" : "offline";
21+
},
22+
},
23+
"node.firmware": {
24+
keys: ["firmware", "release"],
25+
},
26+
"node.baseversion": {
27+
keys: ["firmware", "base"],
28+
},
29+
"node.deprecationStatus": {
30+
keys: ["model"],
31+
modifier: function (d: any) {
32+
if (window.config.deprecated && d && window.config.deprecated.includes(d)) return _.t("deprecation");
33+
if (window.config.eol && d && window.config.eol.includes(d)) return _.t("eol");
34+
return _.t("no");
35+
},
36+
},
37+
"node.hardware": {
38+
keys: ["model"],
39+
},
40+
"node.visible": {
41+
keys: ["location"],
42+
modifier: function (d: any) {
43+
return d && d.longitude && d.latitude ? _.t("yes") : _.t("no");
44+
},
45+
},
46+
"node.update": {
47+
keys: ["autoupdater"],
48+
modifier: function (d: any) {
49+
if (d.enabled) {
50+
return d.branch;
51+
}
52+
return _.t("node.deactivated");
53+
},
54+
},
55+
"node.selectedGatewayIPv4": {
56+
keys: ["gateway"],
57+
modifier: function (nodeid: string | null, data: ObjectsLinksAndNodes) {
58+
let gateway = data.nodeDict[nodeid];
59+
if (gateway) {
60+
return gateway.hostname;
61+
}
62+
return null;
63+
},
64+
},
65+
"node.selectedGatewayIPv6": {
66+
keys: ["gateway6"],
67+
modifier: function (nodeid: string | null, data: ObjectsLinksAndNodes) {
68+
let gateway = data.nodeDict[nodeid];
69+
if (gateway) {
70+
return gateway.hostname;
71+
}
72+
return null;
73+
},
74+
},
75+
"node.domain": {
76+
keys: ["domain"],
77+
modifier: function (d: any) {
78+
if (window.config.domainNames) {
79+
window.config.domainNames.some(function (t) {
80+
if (d === t.domain) {
81+
d = t.name;
82+
return true;
83+
}
84+
});
85+
}
86+
return d;
87+
},
88+
},
89+
};
90+
1691
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
1792

1893
export const Proportions = function (filterManager: ReturnType<typeof DataDistributor>) {
@@ -26,6 +101,14 @@ export const Proportions = function (filterManager: ReturnType<typeof DataDistri
26101
let time: Moment;
27102

28103
let tables: Record<string, TableNode> = {};
104+
// flag set while we apply filters programmatically from the URL hash
105+
let appliedUrlFilters = false;
106+
let applyingFilter = false;
107+
108+
function normalizeKey(s: string | null | undefined) {
109+
if (!s) return "";
110+
return String(s).replace(/\s+/g, " ").trim();
111+
}
29112

30113
function count(nodes: Node[], key: string[], f?: (k: any) => any) {
31114
let dict = {};
@@ -56,6 +139,35 @@ export const Proportions = function (filterManager: ReturnType<typeof DataDistri
56139
};
57140
}
58141

142+
// Watch filter changes and sync the URL accordingly (but ignore when we are
143+
// programmatically applying filters from the hash).
144+
filterManager.watchFilters({
145+
filtersChanged: function (filters: GenericFilter[]) {
146+
const params: { [param: string]: string[] } = {};
147+
148+
filters.forEach(function (f) {
149+
if (!f.getKey) return;
150+
151+
const name = f.getName();
152+
const value = f.getValue();
153+
const negate = f.getNegate();
154+
155+
// Prefix with "!" when negated
156+
const encoded = negate ? `!${value}` : value;
157+
158+
if (!params[name]) {
159+
params[name] = [encoded];
160+
} else {
161+
params[name].push(encoded);
162+
}
163+
});
164+
165+
if (appliedUrlFilters) {
166+
window.router.setParams(params);
167+
}
168+
},
169+
});
170+
59171
function fillTable(name: string, table: TableNode | undefined, data: any[][]): TableNode {
60172
let tableNode: TableNode = table ?? {
61173
element: document.createElement("table"),
@@ -72,7 +184,10 @@ export const Proportions = function (filterManager: ReturnType<typeof DataDistri
72184
let items = data.map(function (data) {
73185
let v = data[1] / max;
74186

75-
let filter = GenericNodeFilter(_.t(name), data[2], data[0], data[3]);
187+
let keys = data[2];
188+
let value = data[0];
189+
let modifierFunction = data[3];
190+
let filter = GenericNodeFilter(name, keys, value, modifierFunction);
76191

77192
let a = h("a", { on: { click: addFilter(filter) } }, data[0]);
78193

@@ -220,8 +335,46 @@ export const Proportions = function (filterManager: ReturnType<typeof DataDistri
220335
return b[1] - a[1];
221336
}),
222337
);
338+
339+
if (!appliedUrlFilters) {
340+
applyFiltersFromHash();
341+
}
223342
};
224343

344+
function applyFiltersFromHash() {
345+
const params = window.router.getParams();
346+
const keys = Object.keys(params);
347+
appliedUrlFilters = true;
348+
if (keys.length === 0) return;
349+
350+
applyingFilter = true;
351+
try {
352+
for (const [param, values] of Object.entries(params)) {
353+
if (!statusFieldMapping[param]) {
354+
console.warn("unknown_filter_param", param);
355+
continue; // continue instead of return to process other params
356+
}
357+
358+
const mapping = statusFieldMapping[param];
359+
360+
values.forEach(function (encodedValue) {
361+
const negate = encodedValue.startsWith("!");
362+
if (negate) {
363+
encodedValue = encodedValue.slice(1);
364+
}
365+
366+
let filter = GenericNodeFilter(param, mapping.keys, normalizeKey(encodedValue), mapping.modifier);
367+
if (negate) {
368+
filter.setNegate(true);
369+
}
370+
filterManager.addFilter(filter);
371+
});
372+
}
373+
} finally {
374+
applyingFilter = false;
375+
}
376+
}
377+
225378
self.render = function render(el: HTMLElement) {
226379
self.renderSingle(el, "node.status", tables.status.element);
227380
self.renderSingle(el, "node.firmware", tables.firmware.element);

lib/utils/router.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export class Router extends Navigo {
106106

107107
if (lang && lang !== this.state.lang && lang === this.language.getLocale(lang)) {
108108
console.debug("Language change reload");
109-
location.hash = "/" + match.url;
109+
location.hash = "/" + match.hashString;
110110
location.reload();
111111
}
112112

@@ -146,7 +146,7 @@ export class Router extends Navigo {
146146
)
147147
.on(
148148
// lang, viewValue, node, link, zoom, lat, lon
149-
/^\/?(\w{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/(-?[\d.]+)\/(-?[\d.]+))?$/,
149+
/^\/?(\w{2})?\/?(map|graph)?\/?([a-f\d]{12})?([a-f\d\-]{25})?\/?(?:(\d+)\/(-?[\d.]+)\/(-?[\d.]+))?(?:\?.*)?$/,
150150
(match?: Match) => {
151151
this.customRoute(match);
152152
},
@@ -206,6 +206,51 @@ export class Router extends Navigo {
206206
return null;
207207
}
208208

209+
// Parse query-like params from the location.hash (everything after '?')
210+
getParams(): { [param: string]: string[] } {
211+
const hash = location.hash || "";
212+
const [, queryString] = hash.split("?");
213+
const out: { [param: string]: string[] } = {};
214+
215+
if (!queryString) {
216+
return out;
217+
}
218+
219+
const params = new URLSearchParams(queryString);
220+
221+
params.forEach(function (value, key) {
222+
// value can be something like "v1,!v2"
223+
const parts = value.split(",").filter(Boolean);
224+
225+
if (!out[key]) {
226+
out[key] = parts;
227+
} else {
228+
out[key] = out[key].concat(parts);
229+
}
230+
});
231+
232+
return out;
233+
}
234+
235+
// Replace params portion in the current hash with provided params object.
236+
// If params is empty, the query portion will be removed.
237+
setParams(params: { [param: string]: string[] }) {
238+
const hash = location.hash || "";
239+
const base = hash.split("?")[0] || "";
240+
const keys = Object.keys(params || {});
241+
if (!keys.length) {
242+
location.hash = base;
243+
return;
244+
}
245+
const qs = new URLSearchParams();
246+
keys.forEach(function (k) {
247+
// Join multiple values for same key with ","
248+
// e.g. map?node.firmware=v1,!v2
249+
qs.set(k, params[k].join(","));
250+
});
251+
location.hash = base + "?" + qs.toString();
252+
}
253+
209254
addTarget(target: Target) {
210255
this.targets.push(target);
211256
}

0 commit comments

Comments
 (0)