fix(health): the user hover card leaves out the login name

It now opens only once the users list is read (admin/analyst): role, today's
spend, workspaces, sessions and last sign-in. A developer gets no card.
This commit is contained in:
2026-11-03 08:50:26 +07:00
parent d0b4f8aaf6
commit b777e08371
5 changed files with 20 additions and 19 deletions
+7 -6
View File
@@ -12,8 +12,8 @@
* busiest processes with who each counts for (history, CPU threads and
* processes are Linux-only reads, so off Linux those cards are left out); and — server mode — each user's
* live CPU / RAM share (a tile per user; click for their busiest processes; hover
* for their login name and — admin/analyst, from `GET /api/users` — role, $ spent
* today, workspaces, sessions and last sign-in). Also exports `useHostStats` (a poll every `pollMs` while
* for — admin/analyst, from `GET /api/users` — their role, $ spent today,
* workspaces, sessions and last sign-in). Also exports `useHostStats` (a poll every `pollMs` while
* the tab is visible; 0 = read once), `Meter`, `pct` and `fmtPct`, which the
* Administration users table shares for each home's share of the homes disk.
* Read-only.
@@ -330,7 +330,7 @@ export function HostSections({ host, heap }: { host: HostState; heap?: HeapInfo
// The user tile whose busiest processes are shown.
const [open, setOpen] = useState<string | null>(null);
// Each account's role and spend for the tiles' hover card. `GET /api/users`
// is admin/analyst only, so a developer's card shows the login name alone.
// is admin/analyst only, so a developer gets no card.
const { seesAll, local } = usePermissions();
const { t: tu } = useTranslation("users");
const [accounts, setAccounts] = useState<Map<number, ServerUserUsage>>(new Map());
@@ -390,10 +390,11 @@ export function HostSections({ host, heap }: { host: HostState; heap?: HeapInfo
return key;
};
const userTip = (g: HostUsageGroup) => {
const lines = [groupName(g)];
if (g.username) lines.push(t("host.userTipLogin", { v: g.username }));
const u = g.userId != null ? accounts.get(g.userId) : undefined;
if (u) {
// No account row (a developer, or not read yet): no card at all.
if (!u) return "";
const lines = [groupName(g)];
{
lines.push(
t("host.userTipRole", { v: tu(`roles.${u.role}`) }) +
(u.disabled ? ` · ${t("host.userTipDisabled")}` : "")
@@ -8,7 +8,7 @@
* switch (`GET /api/host/history`), and the busiest processes with who each
* counts for, the processes of no user in a short list of their own, and
* each user's live CPU / RAM as a tile (their busiest processes on click; on
* hover their login name, and for staff their role and $ spent today);
* hover, for staff, their role and $ spent today);
* off Linux the cards it can never fill are left out.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -186,7 +186,7 @@ describe("HostHealth", () => {
expect(within(tile).getByText("claude")).toBeTruthy();
});
it("names the login, role and today's spend when a user tile is hovered", async () => {
it("names the role and today's spend when a user tile is hovered", async () => {
Object.assign(perms, { local: false });
api.users.list.mockResolvedValue({
users: [
@@ -206,25 +206,27 @@ describe("HostHealth", () => {
});
render(<Tab />);
const tile = await screen.findByTestId("server-user");
await vi.waitFor(() => expect(api.users.list).toHaveBeenCalled());
fireEvent.mouseEnter(within(tile).getByRole("button").parentElement!);
const tip = await screen.findByText(/Login: mia/);
expect(tip.textContent).toContain("Role: Developer");
// The card exists only once the users list has arrived.
const nameRow = () => within(tile).getByRole("button").parentElement!;
const tip = await vi.waitFor(() => {
fireEvent.mouseEnter(nameRow());
return screen.getByText(/Role: Developer/);
});
expect(tip.textContent).not.toContain("Login");
expect(tip.textContent).toMatch(/Today: \$1\.5/);
expect(tip.textContent).toContain("2 workspaces · 5 sessions");
// Only the name row carries it: the CPU bar keeps its own help tip alone.
fireEvent.mouseLeave(within(tile).getByRole("button").parentElement!);
fireEvent.mouseEnter(within(tile).getByRole("meter", { name: "CPU" }));
expect(screen.queryByText(/Login: mia/)).toBeNull();
expect(screen.queryByText(/Role: Developer/)).toBeNull();
});
it("shows a developer only the login name, without reading the users list", async () => {
it("gives a developer no hover card, without reading the users list", async () => {
Object.assign(perms, { seesAll: false, local: false, isAdmin: false });
render(<Tab />);
const tile = await screen.findByTestId("server-user");
fireEvent.mouseEnter(within(tile).getByRole("button").parentElement!);
const tip = await screen.findByText(/Login: mia/);
expect(tip.textContent).not.toContain("Today");
expect(screen.queryByText(/Today:/)).toBeNull();
expect(api.users.list).not.toHaveBeenCalled();
});