feat: port the demo redesign with Manage and Settings tabs

This commit is contained in:
LukeGus committed 2026-10-05 16:23:21 -05:00
1 parent f8151cb885
commit 35c954b79a
128 files changed
+13262 -8143

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));
import { ManageList } from "@/manage/ManageList";
import type { Credential, Host } from "@/types/ui-types";
afterEach(cleanup);
const hosts = [
{ id: "1", name: "web-01", ip: "10.0.0.1", folder: "Prod", tags: [] },
{ id: "2", name: "db-01", ip: "10.0.0.2", folder: "Prod", tags: ["sql"] },
] as unknown as Host[];
const credentials = [
{ id: "c1", name: "deploy", username: "ci", folder: "" },
] as Credential[];
function renderList(overrides: Partial<Parameters<typeof ManageList>[0]> = {}) {
const props = {
editing: false,
mode: "hosts" as const,
onMode: vi.fn(),
hosts,
credentials,
canEditInstanceDefaults: false,
isOnline: () => false,
selectedKey: null,
onPickHost: vi.fn(),
onPickCredential: vi.fn(),
onPickDefaults: vi.fn(),
onAdd: vi.fn(),
...overrides,
};
render(<ManageList {...props} />);
return props;
}
describe("ManageList", () => {
it("lists hosts under their folder and picks one", () => {
const props = renderList();
expect(screen.getByText("Prod")).toBeTruthy();
fireEvent.click(screen.getByText("db-01"));
expect(props.onPickHost).toHaveBeenCalledWith(hosts[1]);
});
it("filters by tags as well as names", () => {
renderList();
fireEvent.change(screen.getByRole("textbox"), { target: { value: "sql" } });
expect(screen.queryByText("web-01")).toBeNull();
expect(screen.getByText("db-01")).toBeTruthy();
});
it("switches mode and adds", () => {
const props = renderList();
fireEvent.click(screen.getByText("nav.credentials"));
expect(props.onMode).toHaveBeenCalledWith("credentials");
fireEvent.click(screen.getByLabelText("hosts.addHost"));
expect(props.onAdd).toHaveBeenCalled();
});
it("lists levels of defaults", () => {
const props = renderList({ mode: "defaults" });
fireEvent.click(screen.getByText("hostDefaults.titleUser"));
expect(props.onPickDefaults).toHaveBeenCalledWith(
expect.objectContaining({ level: "user" }),
);
expect(screen.getByText("Prod")).toBeTruthy();
});
});
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import { defaultsRows } from "@/manage/defaults-rows";
import type { Host } from "@/types/ui-types";
const t = (key: string) => key;
describe("defaultsRows", () => {
it("lists instance defaults only for those who can edit them", () => {
const hosts = [{ folder: "A / B" }, { folder: "" }] as Host[];
expect(defaultsRows(hosts, false, t).map((r) => r.key)).toEqual([
"user",
"folder:A",
"folder:A / B",
]);
expect(defaultsRows(hosts, true, t)[0]).toMatchObject({
key: "admin",
level: "admin",
});
});
});
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { validateHostForm } from "@/manage/host-validation";
describe("validateHostForm", () => {
it("needs an address", () => {
expect(
validateHostForm({ ip: " ", sshPort: 22 }, { enableSsh: true }),
).toEqual({
ip: "manage.errorAddressRequired",
});
});
it("checks the SSH port only when SSH is on", () => {
expect(
validateHostForm({ ip: "h", sshPort: 70000 }, { enableSsh: true }),
).toEqual({ sshPort: "manage.errorPortRange" });
expect(
validateHostForm({ ip: "h", sshPort: 70000 }, { enableSsh: false }),
).toEqual({});
});
});
@@ -0,0 +1,62 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
MANAGE_REQUEST_EVENT,
requestFromLegacyEvent,
requestManage,
resetManageRequests,
takePendingManageRequest,
} from "@/manage/manage-requests";
afterEach(resetManageRequests);
describe("manage requests", () => {
it("parks a request until the tab claims it, once", () => {
const listener = vi.fn();
window.addEventListener(MANAGE_REQUEST_EVENT, listener);
requestManage({ kind: "host", hostId: "7" });
window.removeEventListener(MANAGE_REQUEST_EVENT, listener);
expect(listener).toHaveBeenCalledTimes(1);
expect(takePendingManageRequest()).toEqual({ kind: "host", hostId: "7" });
expect(takePendingManageRequest()).toBeNull();
});
it("keeps only the latest request", () => {
requestManage({ kind: "browse", mode: "credentials" });
requestManage({ kind: "credential", credentialId: null });
expect(takePendingManageRequest()).toEqual({
kind: "credential",
credentialId: null,
});
});
it("translates the old host manager events", () => {
expect(requestFromLegacyEvent("host-manager:add-host")).toEqual({
kind: "host",
hostId: null,
draft: undefined,
});
expect(
requestFromLegacyEvent("host-manager:add-host", { name: "web" }),
).toEqual({ kind: "host", hostId: null, draft: { name: "web" } });
expect(requestFromLegacyEvent("host-manager:edit-host", 12)).toEqual({
kind: "host",
hostId: "12",
});
expect(requestFromLegacyEvent("host-manager:edit-host")).toBeNull();
expect(requestFromLegacyEvent("host-manager:add-credential")).toEqual({
kind: "credential",
credentialId: null,
});
expect(
requestFromLegacyEvent("termix:open-host-defaults", { level: "admin" }),
).toEqual({ kind: "defaults", level: "admin", folderName: undefined });
expect(
requestFromLegacyEvent("host-manager:edit-defaults", {
level: "folder",
folderName: "Prod",
}),
).toEqual({ kind: "defaults", level: "folder", folderName: "Prod" });
expect(requestFromLegacyEvent("something-else")).toBeNull();
});
});
+80
View File
@@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";
import {
allFolderPaths,
buildCredentialManageTree,
buildHostManageTree,
flattenTree,
} from "@/manage/manage-tree";
import type { Credential, Host } from "@/types/ui-types";
function host(id: string, folder: string, extra: Partial<Host> = {}): Host {
return {
id,
name: `host-${id}`,
username: "root",
ip: `10.0.0.${id}`,
port: 22,
folder,
online: false,
cpu: null,
ram: null,
lastAccess: "",
authType: "password",
...extra,
} as Host;
}
describe("manage tree", () => {
it("nests folder paths and sub-hosts", () => {
const tree = buildHostManageTree([
host("1", "Prod / EU"),
host("2", "Prod"),
host("3", "", { parentHostId: "2" }),
]);
expect(allFolderPaths(tree)).toEqual(["Prod", "Prod / EU"]);
const prod = tree.folders[0];
expect(prod.folders[0].items[0].id).toBe("1");
expect(prod.items[0].children[0].id).toBe("3");
});
it("drops a parent loop back to folder placement", () => {
const tree = buildHostManageTree([
host("1", "", { parentHostId: "2" }),
host("2", "", { parentHostId: "1" }),
]);
const total =
tree.items.length + tree.items.flatMap((i) => i.children).length;
expect(total).toBe(2);
});
it("uses the online check it is given", () => {
const tree = buildHostManageTree([host("1", "")], () => true);
expect(tree.items[0].online).toBe(true);
});
it("opens folders holding a search match and hides the rest", () => {
const tree = buildHostManageTree([
host("1", "Prod / EU", { tags: ["db"] }),
host("2", "Staging"),
]);
const rows = flattenTree(tree, new Set(), "db");
expect(
rows.map((r) => (r.kind === "folder" ? r.folder.path : r.item.id)),
).toEqual(["Prod", "Prod / EU", "1"]);
});
it("respects collapsed folders without a search", () => {
const tree = buildHostManageTree([host("1", "Prod")]);
expect(flattenTree(tree, new Set(), "")).toHaveLength(1);
expect(flattenTree(tree, new Set(["Prod"]), "")).toHaveLength(2);
});
it("groups credentials by folder", () => {
const tree = buildCredentialManageTree([
{ id: "c1", name: "deploy", username: "ci", folder: "Keys" },
{ id: "c2", name: "root", username: "root", folder: "" },
] as Credential[]);
expect(tree.folders[0].items[0].note).toBe("ci");
expect(tree.items[0].id).toBe("c2");
});
});