Add map filter groups / more filters / update UI

This commit is contained in:
Jeremy Gallant
2025-04-28 17:38:58 +02:00
parent ff33554716
commit 34db0da87c
3 changed files with 327 additions and 90 deletions
+131 -16
View File
@@ -1,10 +1,11 @@
import { useCallback, useMemo, useState } from "react";
import type { Protobuf } from "@meshtastic/core";
import { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
interface BooleanFilter {
key: string;
label: string;
group: string;
type: "boolean";
predicate: (node: Protobuf.Mesh.NodeInfo, value: boolean) => boolean;
}
@@ -12,6 +13,7 @@ interface BooleanFilter {
interface RangeFilter {
key: string;
label: string;
group: string;
type: "range";
bounds: [number, number];
predicate: (node: Protobuf.Mesh.NodeInfo, value: [number, number]) => boolean;
@@ -20,16 +22,31 @@ interface RangeFilter {
interface SearchFilter {
key: string;
label: string;
group: string;
type: "search";
predicate: (node: Protobuf.Mesh.NodeInfo, value: string) => boolean;
}
export type FilterConfig = BooleanFilter | RangeFilter | SearchFilter;
interface MultiFilter {
key: string;
label: string;
group: string;
type: "multi";
options: string[];
predicate: (node: Protobuf.Mesh.NodeInfo, value: string[]) => boolean;
}
export type FilterConfig =
| BooleanFilter
| RangeFilter
| SearchFilter
| MultiFilter;
export type FilterValueMap = {
[C in FilterConfig as C["key"]]: C extends BooleanFilter ? boolean
: C extends RangeFilter ? [number, number]
: C extends SearchFilter ? string
: C extends MultiFilter ? string[]
: never;
};
@@ -38,6 +55,7 @@ export const filterConfigs: FilterConfig[] = [
{
key: "searchText",
label: "Node name/number",
group: "General",
type: "search",
predicate: (node, text: string) => {
if (!text) return true;
@@ -51,15 +69,10 @@ export const filterConfigs: FilterConfig[] = [
nodeNumHex.includes(search.replace(/!/g, ""));
},
},
{
key: "favOnly",
label: "Show favourites only",
type: "boolean",
predicate: (node, favOnly: boolean) => !favOnly || node.isFavorite,
},
{
key: "hopRange",
label: "Number of hops",
group: "General",
type: "range",
bounds: [0, 7],
predicate: (node, [min, max]: [number, number]) => {
@@ -67,9 +80,47 @@ export const filterConfigs: FilterConfig[] = [
return hops >= min && hops <= max;
},
},
{
key: "lastHeard",
label: "Last heard",
group: "General",
type: "range",
bounds: [0, 864000], // 10 days
predicate: (node, [min, max]: [number, number]) => {
const secondsAgo = Date.now() / 1000 - node.lastHeard;
return (secondsAgo >= min && secondsAgo <= max) ||
(secondsAgo >= min && max == 864000);
},
},
{
key: "favOnly",
label: "Show favourites only",
group: "General",
type: "boolean",
predicate: (node, favOnly: boolean) => !favOnly || node.isFavorite,
},
{
key: "viaMqtt",
label: "Hide MQTT-connected nodes",
group: "General",
type: "boolean",
predicate: (node, hide: boolean) => !hide || !node.viaMqtt,
},
{
key: "snr",
label: "SNR (db)",
group: "Metrics",
type: "range",
bounds: [-20, 10],
predicate: (node, [min, max]: [number, number]) => {
const snr = node.snr ?? -20;
return snr >= min && snr <= max;
},
},
{
key: "channelUtilization",
label: "Channel Utilization (%)",
group: "Metrics",
type: "range",
bounds: [0, 100],
predicate: (node, [min, max]: [number, number]) => {
@@ -80,6 +131,7 @@ export const filterConfigs: FilterConfig[] = [
{
key: "airUtilTx",
label: "Airtime Utilization (%)",
group: "Metrics",
type: "range",
bounds: [0, 100],
predicate: (node, [min, max]: [number, number]) => {
@@ -90,6 +142,7 @@ export const filterConfigs: FilterConfig[] = [
{
key: "battery",
label: "Battery level (%)",
group: "Metrics",
type: "range",
bounds: [0, 101],
predicate: (node, [min, max]: [number, number]) => {
@@ -98,10 +151,52 @@ export const filterConfigs: FilterConfig[] = [
},
},
{
key: "viaMqtt",
label: "Hide MQTT-connected nodes",
type: "boolean",
predicate: (node, hide: boolean) => !hide || !node.viaMqtt,
key: "voltage",
label: "Battery voltage (V)",
group: "Metrics",
type: "range",
bounds: [0.1, 5.0],
predicate: (node, [min, max]: [number, number]) => {
const batt = node.deviceMetrics?.voltage ?? 5;
return batt >= min && batt <= max;
},
},
{
key: "role",
label: "Role",
group: "Role",
type: "multi",
options: Object.keys(Protobuf.Config.Config_DeviceConfig_Role)
.filter((k) => isNaN(Number(k)))
.map((k) => {
const spaced = k.replace(/_/g, " ");
return spaced.charAt(0).toUpperCase() + spaced.slice(1).toLowerCase();
}),
predicate: (node, selected) => {
return selected.map((k) => {
const unSpaced = k.replace(/ /g, "_");
return unSpaced.toUpperCase();
}).includes(
Protobuf.Config.Config_DeviceConfig_Role[node.user?.role ?? 0],
);
},
},
{
key: "hwModel",
label: "Hardware model",
group: "Hardware",
type: "multi",
options: Object.keys(Protobuf.Mesh.HardwareModel)
.filter((k) => isNaN(Number(k)))
.map((k) => {
return k.replace(/_/g, " ");
}),
predicate: (node, selected) => {
return selected.map((k) => {
const unSpaced = k.replace(/ /g, "_");
return unSpaced.toUpperCase();
}).includes(Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]);
},
},
];
@@ -118,6 +213,9 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) {
case "search":
acc[cfg.key] = "";
break;
case "multi":
acc[cfg.key] = cfg.options;
break;
}
return acc;
}, {} as FilterValueMap);
@@ -127,6 +225,15 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) {
defaultState,
);
const groupedFilterConfigs = useMemo(() => {
return filterConfigs.reduce<Record<string, FilterConfig[]>>((acc, cfg) => {
const g = "group" in cfg ? cfg.group : "General";
if (!acc[g]) acc[g] = [];
acc[g].push(cfg);
return acc;
}, {});
}, [filterConfigs]);
const resetFilters = useCallback(() => {
setFilters(defaultState);
}, [defaultState]);
@@ -148,7 +255,7 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) {
if (typeof val !== "boolean") return true;
return cfg.predicate(node, val);
case "range":
case "range": {
if (
!Array.isArray(val) ||
val.length !== 2 ||
@@ -157,8 +264,16 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) {
) {
return true;
}
return cfg.predicate(node, val);
const tuple: [number, number] = [val[0], val[1]];
return cfg.predicate(node, tuple);
}
case "multi": {
const safeArray = (() => {
if (!Array.isArray(val)) return [];
return val.filter((x): x is string => typeof x === "string");
})();
return cfg.predicate(node, safeArray);
}
case "search":
if (typeof val !== "string") return true;
return cfg.predicate(node, val);
@@ -174,6 +289,6 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) {
onFilterChange,
resetFilters,
filteredNodes,
filterConfigs,
groupedFilterConfigs,
};
}