feat(frontend): show workspace storage usage in usage limits panel

Add file storage bar to the CoPilot usage limits popover showing
used/limit bytes, percentage, and file count. Fetches from the
existing GET /workspace/storage/usage endpoint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2026-04-14 21:23:28 -05:00
parent a81f305a74
commit ad971698a0
2 changed files with 91 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { Button } from "@/components/atoms/Button/Button";
import Link from "next/link";
import { formatCents } from "../RateLimitResetDialog/RateLimitResetDialog";
import { useResetRateLimit } from "../../hooks/useResetRateLimit";
import { useWorkspaceStorage } from "./useWorkspaceStorage";
export function formatResetTime(
resetsAt: Date | string,
@@ -73,6 +74,57 @@ function UsageBar({
);
}
export function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
if (bytes < 1024 * 1024 * 1024)
return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
function StorageBar({
usedBytes,
limitBytes,
fileCount,
}: {
usedBytes: number;
limitBytes: number;
fileCount: number;
}) {
if (limitBytes <= 0) return null;
const rawPercent = (usedBytes / limitBytes) * 100;
const percent = Math.min(100, Math.round(rawPercent));
const isHigh = percent >= 80;
const percentLabel =
usedBytes > 0 && percent === 0 ? "<1% used" : `${percent}% used`;
return (
<div className="flex flex-col gap-1">
<div className="flex items-baseline justify-between">
<span className="text-xs font-medium text-neutral-700">
File storage
</span>
<span className="text-[11px] tabular-nums text-neutral-500">
{percentLabel}
</span>
</div>
<div className="text-[10px] text-neutral-400">
{formatBytes(usedBytes)} of {formatBytes(limitBytes)} &middot;{" "}
{fileCount} {fileCount === 1 ? "file" : "files"}
</div>
<div className="h-2 w-full overflow-hidden rounded-full bg-neutral-200">
<div
className={`h-full rounded-full transition-[width] duration-300 ease-out ${
isHigh ? "bg-orange-500" : "bg-blue-500"
}`}
style={{ width: `${Math.max(usedBytes > 0 ? 1 : 0, percent)}%` }}
/>
</div>
</div>
);
}
function ResetButton({
cost,
onCreditChange,
@@ -97,6 +149,19 @@ function ResetButton({
);
}
function WorkspaceStorageSection() {
const { data: storage } = useWorkspaceStorage();
if (!storage || storage.limit_bytes <= 0) return null;
return (
<StorageBar
usedBytes={storage.used_bytes}
limitBytes={storage.limit_bytes}
fileCount={storage.file_count}
/>
);
}
export function UsagePanelContent({
usage,
showBillingLink = true,
@@ -154,6 +219,7 @@ export function UsagePanelContent({
resetsAt={usage.weekly.resets_at}
/>
)}
<WorkspaceStorageSection />
{isDailyExhausted &&
!isWeeklyExhausted &&
resetCost > 0 &&

View File

@@ -0,0 +1,25 @@
import { useQuery } from "@tanstack/react-query";
import { customMutator } from "@/app/api/mutators/custom-mutator";
type StorageUsage = {
used_bytes: number;
limit_bytes: number;
used_percent: number;
file_count: number;
};
export function useWorkspaceStorage() {
return useQuery({
queryKey: ["workspace", "storage", "usage"],
queryFn: async () => {
const res = await customMutator<{
data: StorageUsage;
status: number;
headers: Headers;
}>("/api/workspace/storage/usage", { method: "GET" });
return res.data;
},
staleTime: 30000,
refetchInterval: 60000,
});
}