fix(desktop): align usage table columns CLINE-2996 (#13325)

* fix(desktop): align usage table columns

* fix(desktop): show usage link for empty history
This commit is contained in:
Bee
2026-08-17 17:30:08 -07:00
committed by GitHub
parent 05a6974ef8
commit 6b3f034bce
2 changed files with 87 additions and 19 deletions
@@ -5,10 +5,13 @@ import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AccountView } from "./account-view";
const { invoke } = vi.hoisted(() => ({ invoke: vi.fn() }));
const { invoke, openExternalUrl } = vi.hoisted(() => ({
invoke: vi.fn(),
openExternalUrl: vi.fn(),
}));
vi.mock("@/lib/desktop-client", () => ({
desktopClient: { invoke },
openExternalUrl: vi.fn(),
openExternalUrl,
}));
let container: HTMLDivElement;
@@ -17,6 +20,7 @@ let root: Root;
beforeEach(() => {
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
invoke.mockReset();
openExternalUrl.mockReset();
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
@@ -28,6 +32,60 @@ afterEach(async () => {
vi.restoreAllMocks();
});
describe("AccountView usage table", () => {
it("opens the full usage dashboard from the empty table footer", async () => {
invoke.mockImplementation(
async (_command: string, args?: Record<string, unknown>) => {
switch (args?.operation) {
case "fetchMe":
return {
id: "user-1",
email: "beatrix@cline.bot",
displayName: "Beatrix",
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
organizations: [],
};
case "fetchBalance":
return { balance: 5_000_000 };
case "fetchUserOrganizations":
return [];
case "fetchUsageTransactions":
return [];
default:
return {};
}
},
);
await act(async () => {
root.render(<AccountView />);
});
await vi.waitFor(() => {
expect(container.textContent).toContain("Beatrix");
});
const usageTab = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent === "usage",
);
expect(usageTab).toBeDefined();
await act(async () => usageTab?.click());
await vi.waitFor(() => {
expect(container.textContent).toContain("See More");
expect(container.textContent).toContain("No usage transactions yet.");
});
const seeMoreButton = Array.from(container.querySelectorAll("button")).find(
(button) => button.textContent?.includes("See More"),
);
await act(async () => seeMoreButton?.click());
expect(openExternalUrl).toHaveBeenCalledWith(
"https://app.cline.bot/dashboard/usage",
);
});
});
describe("AccountView signed-out state", () => {
it("renders the sign-in prompt from the typed result and stops fetching account data", async () => {
invoke.mockResolvedValue({
@@ -32,6 +32,7 @@ import { invalidateProviderCatalogCache } from "@/lib/provider-model-catalog";
import { cn } from "@/lib/utils";
const DASHBOARD_URL = "https://app.cline.bot/dashboard";
const USAGE_DASHBOARD_URL = "https://app.cline.bot/dashboard/usage";
const USER_CREDITS_URL =
"https://app.cline.bot/dashboard/account?tab=credits&redirect=true";
const ORGANIZATION_CREDITS_URL =
@@ -740,26 +741,24 @@ export function AccountView() {
</p>
{usageLoading && renderLoading()}
{usageError && renderError(usageError, loadUsage)}
{!usageLoading &&
!usageError &&
usageLoaded &&
(usageTransactions.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
No usage transactions yet.
</p>
) : (
<div className="rounded-lg border border-border overflow-hidden">
<div className="grid grid-cols-[1fr_auto_auto_auto] gap-4 border-b border-border bg-secondary/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
<span>Model</span>
<span className="text-right">Tokens</span>
<span className="text-right">Credits</span>
<span className="text-right">Time</span>
</div>
{!usageLoading && !usageError && usageLoaded && (
<div className="overflow-hidden rounded-lg border border-border">
<div className="grid grid-cols-[minmax(0,1fr)_5.5rem_4.5rem_5.5rem] gap-4 border-b border-border bg-secondary/50 px-4 py-2.5 text-xs font-medium text-muted-foreground">
<span>Model</span>
<span className="text-right">Tokens</span>
<span className="text-right">Credits</span>
<span className="text-right">Time</span>
</div>
{usageTransactions.length === 0 ? (
<p className="px-4 py-8 text-center text-sm text-muted-foreground">
No usage transactions yet.
</p>
) : (
<div className="divide-y divide-border">
{usageTransactions.map((tx) => (
<div
key={tx.id}
className="grid grid-cols-[1fr_auto_auto_auto] gap-4 px-4 py-3 text-sm hover:bg-surface-hover"
className="grid grid-cols-[minmax(0,1fr)_5.5rem_4.5rem_5.5rem] gap-4 px-4 py-3 text-sm hover:bg-surface-hover"
>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
@@ -782,8 +781,19 @@ export function AccountView() {
</div>
))}
</div>
)}
<div className="flex justify-center border-t border-border px-4 py-3">
<button
type="button"
onClick={() => void openExternalUrl(USAGE_DASHBOARD_URL)}
className="flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-surface-hover hover:text-foreground"
>
See More
<ExternalLink className="h-3.5 w-3.5" />
</button>
</div>
))}
</div>
)}
</div>
)}