ui(pool): pick an account's users from its own Edit

Each account row in the Account pool card has a pencil: tick the users
whose sessions run on it (one moves off any other account, unticking sends
one back to the balancer). PUT /api/accounts/:name/users saves the set in one
transaction, audits each change and switches a Claude login into each added
user's home at once. The card-level switch button and pinned list are gone.
This commit is contained in:
2026-10-19 10:45:54 +07:00
parent 776ad6466f
commit c042adc51b
11 changed files with 305 additions and 232 deletions
@@ -5,8 +5,8 @@
* admin adds an account only with their own password typed (the token goes
* in once and is not shown back) — a saved login moved in (the server's own
* included), or a setup-token — and removes one the same way, and an analyst
* sees no add or remove control. An admin switches a user to one account and
* back to automatic, each with their own password; it shows on the accounts.
* sees no add or remove control. An account's Edit ticks the users who run on
* it, saved with the admin's own password; the users show on the account.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -20,7 +20,7 @@ const accountPool = vi.hoisted(() => ({
add: vi.fn(),
remove: vi.fn(),
profiles: vi.fn(),
pin: vi.fn(),
setUsers: vi.fn(),
}));
const users = vi.hoisted(() => ({ list: vi.fn() }));
vi.mock("../../../lib/api", async (importOriginal) => {
@@ -168,53 +168,42 @@ describe("AccountPoolCard", () => {
);
});
it("pins a user to an account and unpins them, with the admin's own password", async () => {
it("sets who runs on an account from its Edit, with the admin's own password", async () => {
accountPool.list.mockResolvedValue({
...POOL,
pins: { claude: [{ userId: 8, username: "leo", account: "team-1" }], codex: [] },
pins: {
claude: [
{ userId: 8, username: "leo", account: "team-1" },
{ userId: 7, username: "mia", account: "team-0" },
],
codex: [],
},
});
accountPool.pin.mockResolvedValue({
pin: { userId: 7, username: "mia", account: "team-1" },
switched: "switched",
accountPool.setUsers.mockResolvedValue({
added: [{ userId: 7, username: "mia", switched: "switched" }],
removed: [{ userId: 8, username: "leo" }],
});
const user = userEvent.setup();
render(<AccountPoolCard isAdmin />);
expect(await screen.findByText("Used by: leo")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /switch a user's account/i }));
expect(screen.queryByRole("button", { name: /switch a user/i })).toBeNull();
await user.click(screen.getByRole("button", { name: "Edit team-1" }));
const dialog = await screen.findByRole("dialog");
await waitFor(() => expect(screen.getByRole("option", { name: "mia" })).toBeInTheDocument());
await user.selectOptions(screen.getByLabelText("User"), "7");
await user.selectOptions(screen.getByLabelText("Account"), "team-1");
const confirm = Array.from(dialog.querySelectorAll("button")).find(
(b) => b.textContent === "Switch"
const leo = await screen.findByRole("checkbox", { name: /leo/ });
const mia = screen.getByRole("checkbox", { name: /mia/ });
expect(leo).toHaveAttribute("aria-checked", "true");
expect(mia).toHaveAttribute("aria-checked", "false");
expect(screen.getByText("now on team-0")).toBeInTheDocument();
await user.click(mia);
await user.click(leo);
const save = Array.from(dialog.querySelectorAll("button")).find(
(b) => b.textContent === "Save"
)!;
expect(confirm).toBeDisabled();
expect(save).toBeDisabled();
await user.type(screen.getByLabelText("Your password"), "boss-pass");
await user.click(confirm);
await user.click(save);
await waitFor(() =>
expect(accountPool.pin).toHaveBeenCalledWith("claude", 7, "team-1", "boss-pass")
);
});
it("unpins from the pinned list", async () => {
accountPool.list.mockResolvedValue({
...POOL,
pins: { claude: [{ userId: 8, username: "leo", account: "team-1" }], codex: [] },
});
accountPool.pin.mockResolvedValue({
pin: { userId: 8, username: "leo", account: null },
switched: null,
});
const user = userEvent.setup();
render(<AccountPoolCard isAdmin />);
await user.click(await screen.findByRole("button", { name: "Back to automatic for leo" }));
const dialog = await screen.findByRole("dialog");
await user.type(screen.getByLabelText("Your password"), "boss-pass");
await user.click(
Array.from(dialog.querySelectorAll("button")).find((b) => b.textContent === "Switch")!
);
await waitFor(() =>
expect(accountPool.pin).toHaveBeenCalledWith("claude", 8, null, "boss-pass")
expect(accountPool.setUsers).toHaveBeenCalledWith("claude", "team-1", [7], "boss-pass")
);
});
});