feat(canvas): scaffold WorkspaceUsage component for #592
Adds WorkspaceUsage component to canvas/src/components/ with three placeholder stat rows (Input tokens, Output tokens, Estimated cost) and a "pending #593" badge. Wires into DetailsTab between the Workspace and Skills sections. No API calls yet — fetch logic will be added once GET /workspaces/:id/metrics lands in #593. 9 tests in WorkspaceUsage.test.tsx; all 548 canvas tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
afc6654dd2
commit
1e0c2eed3b
55
canvas/src/components/WorkspaceUsage.tsx
Normal file
55
canvas/src/components/WorkspaceUsage.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
// WorkspaceUsage — Usage panel for a single workspace.
|
||||
// Currently renders placeholder stat rows.
|
||||
// TODO: fetch GET /workspaces/:id/metrics when #593 lands and replace
|
||||
// placeholder values with real token/cost data from the response.
|
||||
|
||||
export interface WorkspaceUsageProps {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export function WorkspaceUsage({ workspaceId: _workspaceId }: WorkspaceUsageProps) {
|
||||
return (
|
||||
<div
|
||||
className="rounded-md border border-zinc-700 bg-zinc-900 p-3 space-y-2"
|
||||
data-testid="workspace-usage"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
|
||||
Usage
|
||||
</h4>
|
||||
<span
|
||||
className="text-[10px] text-zinc-500 bg-zinc-800 border border-zinc-700 rounded px-1.5 py-0.5"
|
||||
data-testid="usage-pending-badge"
|
||||
>
|
||||
pending #593
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Placeholder stat rows — will be replaced with live data once #593 lands */}
|
||||
<div className="space-y-1.5" data-testid="usage-stats">
|
||||
<StatRow label="Input tokens" value="—" testId="usage-input-tokens" />
|
||||
<StatRow label="Output tokens" value="—" testId="usage-output-tokens" />
|
||||
<StatRow label="Estimated cost" value="—" testId="usage-estimated-cost" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatRow({
|
||||
label,
|
||||
value,
|
||||
testId,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
testId?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-between items-center" data-testid={testId}>
|
||||
<span className="text-xs text-zinc-500">{label}</span>
|
||||
<span className="text-xs text-zinc-400 font-mono">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
canvas/src/components/__tests__/WorkspaceUsage.test.tsx
Normal file
75
canvas/src/components/__tests__/WorkspaceUsage.test.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
// @vitest-environment jsdom
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import { WorkspaceUsage } from "../WorkspaceUsage";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("WorkspaceUsage", () => {
|
||||
it("renders without crashing", () => {
|
||||
const { container } = render(
|
||||
<WorkspaceUsage workspaceId="ws-test-123" />
|
||||
);
|
||||
expect(container.firstChild).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders the Usage heading", () => {
|
||||
render(<WorkspaceUsage workspaceId="ws-test-123" />);
|
||||
expect(screen.getByText("Usage")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders the pending #593 badge", () => {
|
||||
render(<WorkspaceUsage workspaceId="ws-test-123" />);
|
||||
const badge = screen.getByTestId("usage-pending-badge");
|
||||
expect(badge).toBeTruthy();
|
||||
expect(badge.textContent).toBe("pending #593");
|
||||
});
|
||||
|
||||
it("renders the outer container and stats container", () => {
|
||||
render(<WorkspaceUsage workspaceId="ws-test-123" />);
|
||||
expect(screen.getByTestId("workspace-usage")).toBeTruthy();
|
||||
expect(screen.getByTestId("usage-stats")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders Input tokens row with placeholder dash", () => {
|
||||
render(<WorkspaceUsage workspaceId="ws-test-123" />);
|
||||
const row = screen.getByTestId("usage-input-tokens");
|
||||
expect(row).toBeTruthy();
|
||||
expect(row.textContent).toContain("Input tokens");
|
||||
expect(row.textContent).toContain("—");
|
||||
});
|
||||
|
||||
it("renders Output tokens row with placeholder dash", () => {
|
||||
render(<WorkspaceUsage workspaceId="ws-test-123" />);
|
||||
const row = screen.getByTestId("usage-output-tokens");
|
||||
expect(row).toBeTruthy();
|
||||
expect(row.textContent).toContain("Output tokens");
|
||||
expect(row.textContent).toContain("—");
|
||||
});
|
||||
|
||||
it("renders Estimated cost row with placeholder dash", () => {
|
||||
render(<WorkspaceUsage workspaceId="ws-test-123" />);
|
||||
const row = screen.getByTestId("usage-estimated-cost");
|
||||
expect(row).toBeTruthy();
|
||||
expect(row.textContent).toContain("Estimated cost");
|
||||
expect(row.textContent).toContain("—");
|
||||
});
|
||||
|
||||
it("accepts any workspaceId without throwing", () => {
|
||||
const ids = ["", "ws-abc", "00000000-0000-0000-0000-000000000000"];
|
||||
for (const id of ids) {
|
||||
const { unmount } = render(<WorkspaceUsage workspaceId={id} />);
|
||||
expect(screen.getByTestId("workspace-usage")).toBeTruthy();
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not display live token counts or dollar amounts", () => {
|
||||
render(<WorkspaceUsage workspaceId="ws-test-123" />);
|
||||
const stats = screen.getByTestId("usage-stats");
|
||||
// Placeholder state must not contain any digit sequences
|
||||
expect(stats.textContent).not.toMatch(/\d+/);
|
||||
});
|
||||
});
|
||||
@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas";
|
||||
import { StatusDot } from "../StatusDot";
|
||||
import { WorkspaceUsage } from "../WorkspaceUsage";
|
||||
|
||||
interface Props {
|
||||
workspaceId: string;
|
||||
@ -190,6 +191,9 @@ export function DetailsTab({ workspaceId, data }: Props) {
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* Token usage + spend (scaffold — wired to GET /workspaces/:id/metrics once #593 lands) */}
|
||||
<WorkspaceUsage workspaceId={workspaceId} />
|
||||
|
||||
{/* Agent Card / Skills */}
|
||||
{skills.length > 0 && (
|
||||
<Section title="Skills">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user