fix(health): the user hover card sits on the name row only

Over the whole tile it opened together with the CPU bar's own help tip and
the two overlapped.
This commit is contained in:
2026-11-03 08:47:13 +07:00
parent 916505fbac
commit d0b4f8aaf6
2 changed files with 31 additions and 25 deletions
+25 -23
View File
@@ -721,8 +721,10 @@ export function HostSections({ host, heap }: { host: HostState; heap?: HeapInfo
const name = groupName(g);
const isOpen = open === g.key;
return (
<Tip key={g.key} raw={userTip(g)} block>
<div className="space-y-2" data-testid="server-user">
<div key={g.key} className="space-y-2" data-testid="server-user">
{/* The hover card sits on the name row only: over the whole
tile it stacked on the CPU bar's own help tip. */}
<Tip raw={userTip(g)} block>
<button
type="button"
onClick={() => setOpen(isOpen ? null : g.key)}
@@ -743,27 +745,27 @@ export function HostSections({ host, heap }: { host: HostState; heap?: HeapInfo
{t("host.processCount", { n: g.processes })}
</span>
</button>
<HealthBar label="CPU" value={g.cpu} tip={t("host.cpuHelp")} />
<HealthBar label={`RAM · ${formatBytes(g.memory)}`} value={g.memoryPercent} />
{isOpen && (
<ul className="space-y-1 rounded-md bg-surface-2/60 px-2 py-1.5">
{g.top.map((p) => (
<li
key={p.pid}
className="flex justify-between gap-2 text-[10px] font-mono"
>
<span className="truncate text-fg-secondary">
{p.name} <span className="text-fg-muted">#{p.pid}</span>
</span>
<span className="flex-shrink-0 text-fg-muted">
{fmtPct(p.cpu)} · {formatBytes(p.memory)}
</span>
</li>
))}
</ul>
)}
</div>
</Tip>
</Tip>
<HealthBar label="CPU" value={g.cpu} tip={t("host.cpuHelp")} />
<HealthBar label={`RAM · ${formatBytes(g.memory)}`} value={g.memoryPercent} />
{isOpen && (
<ul className="space-y-1 rounded-md bg-surface-2/60 px-2 py-1.5">
{g.top.map((p) => (
<li
key={p.pid}
className="flex justify-between gap-2 text-[10px] font-mono"
>
<span className="truncate text-fg-secondary">
{p.name} <span className="text-fg-muted">#{p.pid}</span>
</span>
<span className="flex-shrink-0 text-fg-muted">
{fmtPct(p.cpu)} · {formatBytes(p.memory)}
</span>
</li>
))}
</ul>
)}
</div>
);
})}
</div>
@@ -207,18 +207,22 @@ describe("HostHealth", () => {
render(<Tab />);
const tile = await screen.findByTestId("server-user");
await vi.waitFor(() => expect(api.users.list).toHaveBeenCalled());
fireEvent.mouseEnter(tile.parentElement!);
fireEvent.mouseEnter(within(tile).getByRole("button").parentElement!);
const tip = await screen.findByText(/Login: mia/);
expect(tip.textContent).toContain("Role: Developer");
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();
});
it("shows a developer only the login name, 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(tile.parentElement!);
fireEvent.mouseEnter(within(tile).getByRole("button").parentElement!);
const tip = await screen.findByText(/Login: mia/);
expect(tip.textContent).not.toContain("Today");
expect(api.users.list).not.toHaveBeenCalled();