mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +00:00
feat: port the demo redesign with Manage and Settings tabs
This commit is contained in:
1 parent
f8151cb885
commit
35c954b79a
128 files changed
+13262
-8143
No files matched your search
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user