mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-10-15 13:49:59 +00:00
Search packet hashes server-side with hashPrefix
This commit is contained in:
+2
-1
@@ -97,7 +97,7 @@ function statsRegionParams(r: StatsRegion): { iatas?: string; region?: string }
|
||||
|
||||
export function getPackets(
|
||||
iatas: string[] | undefined,
|
||||
params?: { cursor?: number; limit?: number; payloadTypes?: number[]; routeTypes?: number[]; scopes?: string[] },
|
||||
params?: { cursor?: number; limit?: number; payloadTypes?: number[]; routeTypes?: number[]; scopes?: string[]; hashPrefix?: string },
|
||||
): Promise<CursorPage<PacketSummary>> {
|
||||
if (noIatas(iatas)) return Promise.resolve(emptyPage());
|
||||
return request("/packets", {
|
||||
@@ -107,6 +107,7 @@ export function getPackets(
|
||||
payloadTypes: params?.payloadTypes?.length ? params.payloadTypes.join(",") : undefined,
|
||||
routeTypes: params?.routeTypes?.length ? params.routeTypes.join(",") : undefined,
|
||||
scopes: params?.scopes?.length ? params.scopes.join(",") : undefined,
|
||||
hashPrefix: params?.hashPrefix,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,12 @@ export function SearchBar({ value, onChange, fields, field, onFieldChange, hideF
|
||||
function handleChange(val: string) {
|
||||
setLocalValue(val);
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
// clearing should restore the full list right away
|
||||
if (val === "") {
|
||||
debounceRef.current = null;
|
||||
onChange(val);
|
||||
return;
|
||||
}
|
||||
debounceRef.current = setTimeout(() => {
|
||||
onChange(val);
|
||||
}, 300);
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface PacketServerFilter {
|
||||
payloadTypes?: number[];
|
||||
routeTypes?: number[];
|
||||
scopes?: string[];
|
||||
hashPrefix?: string;
|
||||
}
|
||||
|
||||
export const EMPTY_FILTERS: PacketFilterState = {
|
||||
|
||||
@@ -118,11 +118,18 @@ export function usePacketFilters() {
|
||||
// The /packets endpoint accepts comma-separated payloadTypes/routeTypes/scopes, so any selected
|
||||
// dimension goes server-side and pagination pulls the correctly-filtered set from the full history.
|
||||
// (observers has no server param, so it stays client-side in matchesFilters, as does the live buffer.)
|
||||
// A 4-16 char hex hash search also goes server-side so older packets can be found.
|
||||
const HASH_PREFIX_RE = /^[0-9a-f]{4,16}$/i;
|
||||
|
||||
export function toServerFilter(filters: PacketFilterState): PacketServerFilter | null {
|
||||
const serverFilter: PacketServerFilter = {};
|
||||
if (filters.payloadTypes.length > 0) serverFilter.payloadTypes = filters.payloadTypes;
|
||||
if (filters.routeTypes.length > 0) serverFilter.routeTypes = filters.routeTypes;
|
||||
if (filters.scopes.length > 0) serverFilter.scopes = filters.scopes;
|
||||
const hashSearch = filters.search.trim();
|
||||
if (filters.searchField === "hash" && HASH_PREFIX_RE.test(hashSearch)) {
|
||||
serverFilter.hashPrefix = hashSearch.toLowerCase();
|
||||
}
|
||||
return Object.keys(serverFilter).length > 0 ? serverFilter : null;
|
||||
}
|
||||
|
||||
@@ -151,7 +158,7 @@ export function matchesFilters(
|
||||
return false;
|
||||
}
|
||||
if (filters.search && filters.searchField === "hash") {
|
||||
const q = filters.search.toLowerCase();
|
||||
const q = filters.search.trim().toLowerCase();
|
||||
if (!packet.packetHash.toLowerCase().includes(q)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,15 @@ describe("getPackets", () => {
|
||||
expect(url.searchParams.has("scopes")).toBe(false);
|
||||
expect(url.searchParams.get("cursor")).toBe("100");
|
||||
expect(url.searchParams.get("limit")).toBe("50");
|
||||
expect(url.searchParams.has("hashPrefix")).toBe(false);
|
||||
});
|
||||
|
||||
it("forwards hashPrefix", async () => {
|
||||
const getUrl = mockFetchOnce({ items: [], nextCursor: null, hasMore: false });
|
||||
|
||||
await getPackets(["YOW"], { hashPrefix: "6ddf62" });
|
||||
|
||||
expect(new URL(getUrl()).searchParams.get("hashPrefix")).toBe("6ddf62");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,3 +18,17 @@ describe("compact observer search", () => {
|
||||
} finally { vi.useRealTimers(); }
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearing the search", () => {
|
||||
it("reports an emptied box immediately instead of waiting out the debounce", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const change = vi.fn();
|
||||
render(<SearchBar value="abcd" onChange={change} fields={[{ value: "hash", label: "Hash" }]} field="hash" onFieldChange={() => {}} hideField inputLabel="Search" />);
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Search" }), { target: { value: "" } });
|
||||
expect(change).toHaveBeenCalledExactlyOnceWith("");
|
||||
act(() => vi.advanceTimersByTime(300));
|
||||
expect(change).toHaveBeenCalledOnce();
|
||||
} finally { vi.useRealTimers(); }
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, within } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, within, act } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { PacketList } from "../../../src/features/packets/PacketList";
|
||||
@@ -367,6 +367,34 @@ describe("PacketList selected packet outside the loaded results", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PacketList hash search", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
usePackets.mockImplementation(basePackets);
|
||||
});
|
||||
|
||||
it("sends a typed hex hash search server-side once the search box settles", () => {
|
||||
vi.useFakeTimers();
|
||||
renderList();
|
||||
fireEvent.change(screen.getByPlaceholderText("Search by hash..."), { target: { value: "6DDF" } });
|
||||
expect(usePackets).toHaveBeenLastCalledWith(false, null);
|
||||
act(() => { vi.runOnlyPendingTimers(); });
|
||||
expect(usePackets).toHaveBeenLastCalledWith(false, { hashPrefix: "6ddf" });
|
||||
});
|
||||
|
||||
it("starts with the URL's hash search already server-side", () => {
|
||||
renderList("/?q=6ddf62");
|
||||
expect(usePackets).toHaveBeenLastCalledWith(false, { hashPrefix: "6ddf62" });
|
||||
});
|
||||
|
||||
it("still filters live packets client-side while a hash search is active", () => {
|
||||
usePackets.mockImplementation(() => ({ ...basePackets(), allPackets: [packet("6ddf0001"), packet("aa110002")] }));
|
||||
renderList("/?q=6ddf");
|
||||
expect(screen.getByRole("button", { name: "6ddf0001" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "aa110002" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("PacketList in French", () => {
|
||||
it("translates the banners and the selected-packet card", async () => {
|
||||
await i18n.changeLanguage("fr");
|
||||
|
||||
@@ -21,6 +21,13 @@ function pkt(over: Partial<PacketSummary>): PacketSummary {
|
||||
};
|
||||
}
|
||||
|
||||
describe("matchesFilters — hash search", () => {
|
||||
it("matches a trimmed, case-insensitive substring of the hash", () => {
|
||||
expect(matchesFilters(pkt({ packetHash: "abcd1234" }), { ...EMPTY_FILTERS, search: " CD12 " })).toBe(true);
|
||||
expect(matchesFilters(pkt({ packetHash: "abcd1234" }), { ...EMPTY_FILTERS, search: "ffff" })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("matchesFilters — scope", () => {
|
||||
it("ignores scope when no scope filter is set", () => {
|
||||
expect(matchesFilters(pkt({ scope: "#bc" }), EMPTY_FILTERS)).toBe(true);
|
||||
@@ -83,6 +90,23 @@ describe("toServerFilter", () => {
|
||||
expect(toServerFilter({ ...EMPTY_FILTERS, observers: ["o1"] })).toBeNull();
|
||||
expect(toServerFilter({ ...EMPTY_FILTERS, search: "ab" })).toBeNull();
|
||||
});
|
||||
|
||||
it.each(["abcd", "ABCDE", "6ddf62fb5b9a969e", " abcd "])("sends a %j hash search server-side as a trimmed lowercase hashPrefix", (search) => {
|
||||
expect(toServerFilter({ ...EMPTY_FILTERS, search })).toEqual({ hashPrefix: search.trim().toLowerCase() });
|
||||
});
|
||||
|
||||
it.each(["abc", "6ddf62fb5b9a969e0", "abcg", "ab cd"])("keeps the %j hash search client-side only", (search) => {
|
||||
expect(toServerFilter({ ...EMPTY_FILTERS, search })).toBeNull();
|
||||
});
|
||||
|
||||
it("never sends a path search as a hashPrefix", () => {
|
||||
expect(toServerFilter({ ...EMPTY_FILTERS, search: "abcd", searchField: "path" })).toBeNull();
|
||||
});
|
||||
|
||||
it("combines hashPrefix with the other server filters", () => {
|
||||
expect(toServerFilter({ ...EMPTY_FILTERS, payloadTypes: [4] as PayloadTypeValue[], search: "abcd" }))
|
||||
.toEqual({ payloadTypes: [4], hashPrefix: "abcd" });
|
||||
});
|
||||
});
|
||||
|
||||
function routerAt(url: string) {
|
||||
|
||||
Reference in New Issue
Block a user