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:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -183,7 +183,6 @@
|
||||
"agenthub": "AgentHub",
|
||||
"usersTitle": "Users · CPU / RAM now",
|
||||
"usersCount": "{{n}} users",
|
||||
"userTipLogin": "Login: {{v}}",
|
||||
"userTipRole": "Role: {{v}}",
|
||||
"userTipToday": "Today: {{cost}} · {{tokens}} tokens",
|
||||
"userTipWork": "{{w}} workspaces · {{s}} sessions",
|
||||
|
||||
@@ -183,7 +183,6 @@
|
||||
"agenthub": "AgentHub",
|
||||
"usersTitle": "Người dùng · CPU / RAM hiện tại",
|
||||
"usersCount": "{{n}} người dùng",
|
||||
"userTipLogin": "Tên đăng nhập: {{v}}",
|
||||
"userTipRole": "Vai trò: {{v}}",
|
||||
"userTipToday": "Hôm nay: {{cost}} · {{tokens}} token",
|
||||
"userTipWork": "{{w}} workspace · {{s}} phiên",
|
||||
|
||||
Reference in New Issue
Block a user