Improvements to node filtering and storage (#839)

* Improves node filtering and voltage display

Ensures voltage values are always displayed as positive.

Enhances node filtering logic to handle unknown or undefined values, preventing nodes from being unexpectedly hidden.

Updates UI labels for clarity.

Improves Nodes page responsiveness by debouncing node updates and optimizing selector usage, preventing unnecessary re-renders.

Adds validation warnings for key conflicts between nodes.

* Update packages/web/src/core/stores/nodeDBStore/nodeDBStore.test.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/core/stores/nodeDBStore/nodeValidation.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Revert copilot suggestion

* Review changes

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jeremy Gallant
2025-09-12 17:21:13 +02:00
committed by GitHub
co-authored by Copilot philon-
parent 52fd2f712c
commit 3e2fe721d3
14 changed files with 488 additions and 100 deletions
@@ -174,7 +174,10 @@ export const NodeDetailsDialog = ({
{
key: "voltage",
label: t("nodeDetails.voltage"),
value: node.deviceMetrics?.voltage,
value:
typeof node.deviceMetrics?.voltage === "number"
? Math.abs(node.deviceMetrics?.voltage)
: undefined,
format: (val: number) => `${val.toFixed(2)}V`,
},
];
+4 -1
View File
@@ -205,7 +205,10 @@ export const Sidebar = ({ children }: SidebarProps) => {
}
deviceMetrics={{
batteryLevel: myNode.deviceMetrics?.batteryLevel,
voltage: myNode.deviceMetrics?.voltage,
voltage:
typeof myNode.deviceMetrics?.voltage === "number"
? Math.abs(myNode.deviceMetrics?.voltage)
: undefined,
}}
/>
)}
+5 -3
View File
@@ -50,6 +50,8 @@ export function Slider({
onValueCommit?.(newValue);
};
const thumbIds = currentValue.map((_, idx) => `${internalId}-thumb-${idx}`); // Unique IDs for each thumb, pregenerated to please the linter
return (
<SliderPrimitive.Root
className={cn(
@@ -79,14 +81,14 @@ export function Slider({
)}
/>
</SliderPrimitive.Track>
{currentValue.map((_) => (
{currentValue.map((_, idx) => (
<SliderPrimitive.Thumb
key={`${internalId}-thumb`}
key={thumbIds[idx]}
className={cn(
"block w-4 h-4 rounded-full bg-white border border-slate-400 shadow-md",
thumbClassName,
)}
aria-label={`Thumb ${internalId}`}
aria-label={`Thumb ${idx + 1}`}
/>
))}
</SliderPrimitive.Root>
@@ -67,25 +67,33 @@ export function useFilterNode() {
...filterOverrides,
};
if (!node.user) {
return false;
}
const nodeName = filterState.nodeName.toLowerCase();
if (
nodeName &&
!(
node.user?.shortName.toLowerCase().includes(nodeName) ||
node.user?.longName.toLowerCase().includes(nodeName) ||
node.num.toString().includes(nodeName) ||
numberToHexUnpadded(node.num).includes(nodeName.replace(/!/g, ""))
)
) {
return false;
if (nodeName) {
const short = node.user?.shortName?.toLowerCase() ?? "";
const long = node.user?.longName?.toLowerCase() ?? "";
const numStr = node.num.toString();
const hex = numberToHexUnpadded(node.num);
if (
!short.includes(nodeName) &&
!long.includes(nodeName) &&
!numStr.includes(nodeName) &&
!hex.includes(nodeName.replace(/!/g, ""))
) {
return false;
}
}
const hops = node.hopsAway ?? 7;
if (hops < filterState.hopsAway[0] || hops > filterState.hopsAway[1]) {
if (
(node.hopsAway === undefined &&
!shallowEqualArray(
filterState.hopsAway,
defaultFilterValues.hopsAway,
)) || // If hops are unknown, hide node if state is not default
hops < filterState.hopsAway[0] ||
hops > filterState.hopsAway[1]
) {
return false;
}
@@ -96,8 +104,13 @@ export function useFilterNode() {
return false;
}
const secondsAgo = Date.now() / 1000 - (node.lastHeard ?? 0);
const secondsAgo = Math.max(0, Date.now() / 1000 - (node.lastHeard ?? 0));
if (
(node.lastHeard === 0 &&
!shallowEqualArray(
filterState.lastHeard,
defaultFilterValues.lastHeard,
)) || // If lastHeard is unknown (0), hide node if state is not default
secondsAgo < filterState.lastHeard[0] ||
(secondsAgo > filterState.lastHeard[1] &&
filterState.lastHeard[1] !== defaultFilterValues.lastHeard[1])
@@ -128,6 +141,8 @@ export function useFilterNode() {
const snr = node.snr ?? -20;
if (
(node.snr === undefined &&
!shallowEqualArray(filterState.snr, defaultFilterValues.snr)) ||
(snr < filterState.snr[0] &&
filterState.snr[0] !== defaultFilterValues.snr[0]) ||
(snr > filterState.snr[1] &&
@@ -138,6 +153,11 @@ export function useFilterNode() {
const channelUtilization = node.deviceMetrics?.channelUtilization ?? 0;
if (
(node.deviceMetrics?.channelUtilization === undefined &&
!shallowEqualArray(
filterState.channelUtilization,
defaultFilterValues.channelUtilization,
)) ||
channelUtilization < filterState.channelUtilization[0] ||
channelUtilization > filterState.channelUtilization[1]
) {
@@ -146,6 +166,11 @@ export function useFilterNode() {
const airUtilTx = node.deviceMetrics?.airUtilTx ?? 0;
if (
(node.deviceMetrics?.airUtilTx === undefined &&
!shallowEqualArray(
filterState.airUtilTx,
defaultFilterValues.airUtilTx,
)) ||
airUtilTx < filterState.airUtilTx[0] ||
airUtilTx > filterState.airUtilTx[1]
) {
@@ -154,14 +179,24 @@ export function useFilterNode() {
const batt = node.deviceMetrics?.batteryLevel ?? 101;
if (
(node.deviceMetrics?.batteryLevel === undefined &&
!shallowEqualArray(
filterState.batteryLevel,
defaultFilterValues.batteryLevel,
)) ||
batt < filterState.batteryLevel[0] ||
batt > filterState.batteryLevel[1]
) {
return false;
}
const voltage = node.deviceMetrics?.voltage ?? 0;
const voltage = Math.abs(node.deviceMetrics?.voltage ?? 0);
if (
(node.deviceMetrics?.voltage === undefined &&
!shallowEqualArray(
filterState.voltage,
defaultFilterValues.voltage,
)) ||
voltage < filterState.voltage[0] ||
(voltage > filterState.voltage[1] &&
filterState.voltage[1] !== defaultFilterValues.voltage[1])
@@ -170,14 +205,25 @@ export function useFilterNode() {
}
const role: Protobuf.Config.Config_DeviceConfig_Role =
node.user.role ?? Protobuf.Config.Config_DeviceConfig_Role.CLIENT;
if (!filterState.role.includes(role)) {
node.user?.role ?? Protobuf.Config.Config_DeviceConfig_Role.CLIENT;
if (
(node.user?.role === undefined &&
!shallowEqualArray(filterState.role, defaultFilterValues.role)) ||
!filterState.role.includes(role)
) {
return false;
}
const hwModel: Protobuf.Mesh.HardwareModel =
node.user.hwModel ?? Protobuf.Mesh.HardwareModel.UNSET;
if (!filterState.hwModel.includes(hwModel)) {
node.user?.hwModel ?? Protobuf.Mesh.HardwareModel.UNSET;
if (
(node.user?.hwModel === undefined &&
!shallowEqualArray(
filterState.hwModel,
defaultFilterValues.hwModel,
)) ||
!filterState.hwModel.includes(hwModel)
) {
return false;
}