mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
fix: simplify host status to online/offline and keep it live without a refresh
This commit is contained in:
1 parent
2646b649d7
commit
0a436ab3d8
48 files changed
+549
-789
No files matched your search
@@ -49,7 +49,7 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// The server keys statuses by sync id; its own ids mean nothing here.
|
||||
remoteCoreApiMock.get.mockResolvedValue({
|
||||
data: { "sync-2": { status: "reachable" }, "sync-9": { status: "x" } },
|
||||
data: { "sync-2": { status: "offline" }, "sync-9": { status: "x" } },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("status check origin routing", () => {
|
||||
|
||||
await expect(getAllServerStatuses()).resolves.toEqual({
|
||||
1: { status: "online" },
|
||||
2: { status: "reachable" },
|
||||
2: { status: "offline" },
|
||||
});
|
||||
|
||||
expect(sshHostApiMock.get).toHaveBeenCalledWith("/status", {
|
||||
@@ -91,6 +91,19 @@ describe("status check origin routing", () => {
|
||||
__silentRetry: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips the tick when the host list cannot be read", async () => {
|
||||
sshHostApiMock.get.mockImplementation(async (path: string) => {
|
||||
if (path === "/db/host") throw new Error("down");
|
||||
return { data: {} };
|
||||
});
|
||||
|
||||
await expect(getAllServerStatuses()).rejects.toThrow("down");
|
||||
expect(sshHostApiMock.get).not.toHaveBeenCalledWith(
|
||||
"/status",
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("status check routes", () => {
|
||||
|
||||
@@ -66,8 +66,8 @@ describe("getStatusClasses", () => {
|
||||
expect(getStatusClasses(true, "accent", "dot")).toContain("accent-brand");
|
||||
});
|
||||
|
||||
it("draws reachable as a warning in both schemes", () => {
|
||||
expect(getStatusClasses("reachable", "accent", "dot")).toContain("amber");
|
||||
expect(getStatusClasses("reachable", "status", "dot")).toContain("amber");
|
||||
it("draws an unchecked host as loading", () => {
|
||||
expect(getStatusClasses("unknown", "accent", "dot")).toContain("pulse");
|
||||
expect(getStatusClasses("unknown", "status", "dot")).toContain("pulse");
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { ServerStatusStore } from "../../lib/server-status-store";
|
||||
describe("ServerStatusStore", () => {
|
||||
it("notifies only listeners for hosts whose status value changed", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.setEnabledHostIds(new Set([1, 2, 3]));
|
||||
|
||||
const on1 = vi.fn();
|
||||
const on2 = vi.fn();
|
||||
@@ -43,7 +42,6 @@ describe("ServerStatusStore", () => {
|
||||
|
||||
it("does not notify when the status map is unchanged", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.setEnabledHostIds(new Set([1]));
|
||||
store.applyStatuses(
|
||||
new Map([[1, { status: "online", lastChecked: "t1" }]]),
|
||||
);
|
||||
@@ -57,47 +55,23 @@ describe("ServerStatusStore", () => {
|
||||
expect(store.getStatus(1)).toBe("online");
|
||||
});
|
||||
|
||||
it("returns offline for disabled hosts", () => {
|
||||
it("calls a host it has no result for unknown, never offline", () => {
|
||||
const store = new ServerStatusStore();
|
||||
expect(store.getStatus(5)).toBe("unknown");
|
||||
});
|
||||
|
||||
it("forgets everything on clear", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.applyStatuses(new Map([[5, { status: "online", lastChecked: "t" }]]));
|
||||
expect(store.getStatus(5)).toBe("offline");
|
||||
store.setEnabledHostIds(new Set([5]));
|
||||
expect(store.getStatus(5)).toBe("online");
|
||||
});
|
||||
|
||||
it("preserves reachable as distinct from authenticated online", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.setEnabledHostIds(new Set([7]));
|
||||
store.applyStatuses(
|
||||
new Map([[7, { status: "reachable", lastChecked: "t" }]]),
|
||||
);
|
||||
expect(store.getStatus(7)).toBe("reachable");
|
||||
});
|
||||
|
||||
it("notifies when a status reason changes without a status change", () => {
|
||||
const store = new ServerStatusStore();
|
||||
store.setEnabledHostIds(new Set([7]));
|
||||
store.applyStatuses(
|
||||
new Map([[7, { status: "reachable", lastChecked: "t1" }]]),
|
||||
);
|
||||
store.setInitialLoadComplete(true);
|
||||
const listener = vi.fn();
|
||||
store.subscribeHost(7, listener);
|
||||
store.subscribeHost(5, listener);
|
||||
|
||||
store.applyStatuses(
|
||||
new Map([
|
||||
[
|
||||
7,
|
||||
{
|
||||
status: "reachable",
|
||||
reason: "host_key_changed",
|
||||
lastChecked: "t2",
|
||||
},
|
||||
],
|
||||
]),
|
||||
);
|
||||
store.clear();
|
||||
|
||||
expect(store.getStatus(5)).toBe("unknown");
|
||||
expect(store.getInitialLoadComplete()).toBe(false);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(store.getHostSnapshot(7)).toBe("reachable:host_key_changed");
|
||||
});
|
||||
|
||||
it("emits meta listeners when initial load completes", () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Host, HostFolder } from "@/types/ui-types";
|
||||
import { withLiveHostStatus, withLiveStatus } from "@/sidebar/live-host-status";
|
||||
|
||||
const host = (id: number, extra: Partial<Host> = {}) =>
|
||||
({ id: String(id), name: `h${id}`, online: false, ...extra }) as Host;
|
||||
|
||||
const statuses: Record<number, "online" | "offline"> = {
|
||||
1: "online",
|
||||
2: "offline",
|
||||
};
|
||||
const getStatus = (id: number) => statuses[id] ?? "unknown";
|
||||
|
||||
describe("withLiveStatus", () => {
|
||||
it("copies live statuses into nested folders", () => {
|
||||
const tree: HostFolder = {
|
||||
name: "root",
|
||||
children: [host(1), { name: "f", children: [host(2), host(3)] }],
|
||||
};
|
||||
const live = withLiveStatus(tree, getStatus);
|
||||
const [first, folder] = live.children as [Host, HostFolder];
|
||||
const [second, third] = folder.children as Host[];
|
||||
expect(first).toMatchObject({ status: "online", online: true });
|
||||
expect(second).toMatchObject({ status: "offline", online: false });
|
||||
expect(third).toMatchObject({ status: "unknown", online: false });
|
||||
});
|
||||
|
||||
it("gives a host with status checks off no status", () => {
|
||||
const [live] = withLiveHostStatus(
|
||||
[host(1, { statusCheckEnabled: false })],
|
||||
getStatus,
|
||||
);
|
||||
expect(live.status).toBeUndefined();
|
||||
expect(live.online).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the same object when nothing changed", () => {
|
||||
const same = host(1, { status: "online", online: true });
|
||||
expect(withLiveHostStatus([same], getStatus)[0]).toBe(same);
|
||||
});
|
||||
});
|
||||
@@ -27,12 +27,11 @@ vi.mock("react-i18next", () => ({
|
||||
|
||||
vi.mock("@/lib/ServerStatusContext", () => ({
|
||||
useHostStatus: () => null,
|
||||
useHostStatusReason: () => null,
|
||||
useServerStatus: () => ({
|
||||
getStatus: () => "online",
|
||||
initialLoadComplete: true,
|
||||
}),
|
||||
useServerStatusMeta: () => ({ initialLoadComplete: true, isLoading: false }),
|
||||
useServerStatusMeta: () => ({ initialLoadComplete: true }),
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-status-color-scheme", () => ({
|
||||
|
||||
@@ -27,24 +27,24 @@ function makeHost(overrides: Partial<Host> = {}): Host {
|
||||
// not the key paths the translator falls back to when a resource is missing.
|
||||
const t = (key: string): string =>
|
||||
({
|
||||
"hosts.status.available": "Available",
|
||||
"hosts.status.reachable": "Reachable, not authenticated",
|
||||
"hosts.status.online": "Online",
|
||||
"hosts.status.checking": "Checking",
|
||||
"hosts.status.offline": "Offline",
|
||||
"hosts.status.monitoringDisabled": "Monitoring disabled",
|
||||
})[key] ?? key;
|
||||
|
||||
describe("buildStatusTooltip", () => {
|
||||
it("returns the translated 'Available' label for online status", () => {
|
||||
it("returns the translated 'Online' label for online status", () => {
|
||||
const host = makeHost();
|
||||
const tooltip = buildStatusTooltip(host, "online", t);
|
||||
expect(tooltip).toContain("Available");
|
||||
expect(tooltip).toContain("Online");
|
||||
expect(tooltip).not.toContain("hosts.status.");
|
||||
});
|
||||
|
||||
it("returns the translated 'Reachable, not authenticated' label for reachable status", () => {
|
||||
it("returns 'Checking' before the host has been checked", () => {
|
||||
const host = makeHost();
|
||||
const tooltip = buildStatusTooltip(host, "reachable", t);
|
||||
expect(tooltip).toContain("Reachable, not authenticated");
|
||||
const tooltip = buildStatusTooltip(host, "unknown", t);
|
||||
expect(tooltip).toContain("Checking");
|
||||
expect(tooltip).not.toContain("hosts.status.");
|
||||
});
|
||||
|
||||
@@ -78,13 +78,13 @@ describe("buildStatusTooltip", () => {
|
||||
dispose();
|
||||
expect(tooltip).toContain("SSH");
|
||||
expect(tooltip).toContain("Demo Desktop");
|
||||
expect(tooltip).toContain("Available");
|
||||
expect(tooltip).toContain("Online");
|
||||
});
|
||||
|
||||
it("returns just the status label when no protocols are enabled", () => {
|
||||
const host = makeHost({ enableSsh: false });
|
||||
const tooltip = buildStatusTooltip(host, "online", t);
|
||||
expect(tooltip).toBe("Available");
|
||||
expect(tooltip).toBe("Online");
|
||||
});
|
||||
|
||||
it("does not render key paths when a translator is supplied", () => {
|
||||
|
||||
Reference in new issue
Block a user