admin: live and query log pages show client names
This commit is contained in:
@@ -1,8 +1,49 @@
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import type { LiveQueryEvent } from "@/lib/types";
|
||||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createQueryClient } from "@/lib/queryClient";
|
||||
import type { Client, LiveQueryEvent } from "@/lib/types";
|
||||
import { FakeEventSource } from "./fakeEventSource";
|
||||
import LiveLogPage from "./LiveLogPage";
|
||||
|
||||
function client(ip: string, name: string, learnedName: string): Client {
|
||||
return {
|
||||
id: Number(ip.split(".").pop()),
|
||||
ip,
|
||||
name,
|
||||
learned_name: learnedName,
|
||||
group_id: 1,
|
||||
group: "default",
|
||||
hand_edited: name !== "",
|
||||
first_seen: 1_700_000_000,
|
||||
last_seen: 1_700_000_100,
|
||||
};
|
||||
}
|
||||
|
||||
const CLIENTS: Client[] = [
|
||||
client("192.0.2.10", "Kitchen Pi", "pi.lan"),
|
||||
client("192.0.2.11", "", "laptop.lan"),
|
||||
client("192.0.2.12", "", ""),
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
if (String(input) !== "/api/clients") {
|
||||
return new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 });
|
||||
}
|
||||
return new Response(JSON.stringify({ clients: CLIENTS }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
function frame(ts: number, domain: string, overrides: Partial<LiveQueryEvent> = {}): { data: string } {
|
||||
const payload: LiveQueryEvent = {
|
||||
ts,
|
||||
@@ -26,7 +67,11 @@ function renderPage() {
|
||||
sources.push(es);
|
||||
return es;
|
||||
};
|
||||
render(<LiveLogPage createEventSource={createEventSource} />);
|
||||
render(
|
||||
<QueryClientProvider client={createQueryClient()}>
|
||||
<LiveLogPage createEventSource={createEventSource} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return sources;
|
||||
}
|
||||
|
||||
@@ -71,6 +116,72 @@ test("streams rows, flags blocked ones, and freezes the display", () => {
|
||||
expect(screen.getByText("later.example")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("resolves each row's client to its display name, keeping the IP as the tooltip", async () => {
|
||||
const sources = renderPage();
|
||||
act(() => sources[0]!.emit("open"));
|
||||
act(() => {
|
||||
sources[0]!.emit("query", frame(1000, "named.example", { client_ip: "192.0.2.10" }));
|
||||
sources[0]!.emit("query", frame(1001, "learned.example", { client_ip: "192.0.2.11" }));
|
||||
sources[0]!.emit("query", frame(1002, "nameless.example", { client_ip: "192.0.2.12" }));
|
||||
sources[0]!.emit("query", frame(1003, "stranger.example", { client_ip: "192.0.2.99" }));
|
||||
});
|
||||
|
||||
// A hand-typed name wins outright; the learned name never surfaces for it.
|
||||
const named = await screen.findByText("Kitchen Pi");
|
||||
expect(named.getAttribute("title")).toBe("192.0.2.10");
|
||||
expect(screen.queryByText("pi.lan")).toBeNull();
|
||||
|
||||
// The cell holds the learned name followed by the tag, so the match is on
|
||||
// the containing span rather than on a bare text node.
|
||||
const learned = screen.getByText(
|
||||
(content, element) => element?.tagName === "SPAN" && content.startsWith("laptop.lan"),
|
||||
);
|
||||
expect(learned.getAttribute("title")).toBe("192.0.2.11");
|
||||
// The affordance is text, not colour, so a screen reader announces it too.
|
||||
expect(within(learned).getByText("learned")).toBeTruthy();
|
||||
|
||||
// A known client with neither name, and a client the loaded list has never
|
||||
// seen, both fall back to the bare address with no tooltip standing in.
|
||||
const nameless = screen.getByText("192.0.2.12");
|
||||
expect(nameless.getAttribute("title")).toBeNull();
|
||||
const stranger = screen.getByText("192.0.2.99");
|
||||
expect(stranger.getAttribute("title")).toBeNull();
|
||||
expect(screen.getByText("stranger.example").closest("tr")?.textContent).toContain("192.0.2.99");
|
||||
});
|
||||
|
||||
test("rows stream in as bare IPs while the client list is still loading", async () => {
|
||||
let releaseClients: () => void = () => {};
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(
|
||||
(input: RequestInfo | URL) =>
|
||||
new Promise<Response>((resolve) => {
|
||||
if (String(input) !== "/api/clients") {
|
||||
resolve(new Response(JSON.stringify({ error: "not stubbed" }), { status: 404 }));
|
||||
return;
|
||||
}
|
||||
releaseClients = () =>
|
||||
resolve(
|
||||
new Response(JSON.stringify({ clients: CLIENTS }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const sources = renderPage();
|
||||
act(() => sources[0]!.emit("open"));
|
||||
act(() => sources[0]!.emit("query", frame(1000, "named.example", { client_ip: "192.0.2.10" })));
|
||||
|
||||
expect(screen.getByText("192.0.2.10")).toBeTruthy();
|
||||
expect(screen.queryByText("Kitchen Pi")).toBeNull();
|
||||
|
||||
releaseClients();
|
||||
expect(await screen.findByText("Kitchen Pi")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("repeated connection failures show the viewer-cap state with a retry button", () => {
|
||||
const sources = renderPage();
|
||||
act(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
import { useClientNames } from "@/features/clients/clientNames";
|
||||
import { QueryCells, QueryTableHead } from "@/features/queries/QueryLogPage";
|
||||
import { RING_CAPACITY } from "./ringBuffer";
|
||||
import { useLiveQueries, type EventSourceFactory, type StreamStatus } from "./useLiveQueries";
|
||||
@@ -168,6 +169,7 @@ function StatusPill({ status }: { status: StreamStatus }) {
|
||||
* filling; Resume shows the current buffer (anything pushed out meanwhile is gone). */
|
||||
export default function LiveLogPage({ createEventSource }: { createEventSource?: EventSourceFactory } = {}) {
|
||||
const live = useLiveQueries({ createEventSource });
|
||||
const clientNames = useClientNames();
|
||||
|
||||
return (
|
||||
<section>
|
||||
@@ -240,7 +242,7 @@ export default function LiveLogPage({ createEventSource }: { createEventSource?:
|
||||
<tbody>
|
||||
{live.rows.map((row) => (
|
||||
<tr key={row.key} {...stylex.props(styles.row, row.blocked && styles.rowBlocked)}>
|
||||
<QueryCells row={row} />
|
||||
<QueryCells row={row} clientNames={clientNames} />
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user