open-swe/ui/src/routes/usage.tsx
Caroline di Vittorio c9960a7253
fix: let usage table expand to fill available width (#1465)
* fix: let usage table expand to fill available width

The usage page was constrained to max-w-3xl while the leaderboard table
needs a 760px minimum for its 7 columns, so it always overflowed and
forced horizontal scrolling. Add an optional maxWidthClassName prop to
AppShell and widen the usage page to max-w-5xl so the table expands when
space allows, keeping overflow-x-auto as a narrow-screen fallback.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* refactor: use cn-merged className override on AppShell

Replace the single-purpose maxWidthClassName prop with a general
className prop merged via cn (twMerge), matching the shadcn pattern. This
lets callers override any of the content-container classes ad hoc rather
than adding a new prop per override. Usage page passes className=max-w-5xl.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
2026-06-09 11:14:18 -07:00

337 lines
11 KiB
TypeScript

import { Navigate, createFileRoute } from "@tanstack/react-router"
import { useQuery } from "@tanstack/react-query"
import type {
ReviewerStatsPayload,
UsageLeaderboardPeriod,
UsageLeaderboardRow,
} from "@/lib/api"
import { AppShell, SettingsSection } from "@/components/AppShell"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Skeleton } from "@/components/ui/skeleton"
import { api } from "@/lib/api"
import { useSession } from "@/lib/session"
export const Route = createFileRoute("/usage")({
validateSearch: (search: Record<string, unknown>) => ({
period: typeof search.period === "string" ? search.period : undefined,
}),
component: UsagePage,
})
const PERIOD_LABELS: Record<UsageLeaderboardPeriod, string> = {
"7d": "Last 7 days",
"30d": "Last 30 days",
all: "All time",
}
function UsagePage() {
const session = useSession()
const period =
(Route.useSearch().period as UsageLeaderboardPeriod | undefined) ?? "30d"
const navigate = Route.useNavigate()
const activePeriod: UsageLeaderboardPeriod = ["7d", "30d", "all"].includes(
period
)
? period
: "30d"
const leaderboard = useQuery({
queryKey: ["usageLeaderboard", activePeriod],
queryFn: () => api.usageLeaderboard(activePeriod, 10),
enabled: !!session.data,
staleTime: 5 * 60 * 1000,
})
if (session.isLoading) {
return (
<main className="p-6">
<Skeleton className="h-64 w-full" />
</main>
)
}
if (!session.data) return <Navigate to="/login" />
return (
<AppShell user={session.data} title="Usage" className="max-w-5xl">
<SettingsSection
title="Agent leaderboard"
description="Ranked by agent lines of code, then PRs opened and agent runs."
action={
<Select
value={activePeriod}
onValueChange={(value) =>
navigate({
to: "/usage",
search: { period: value as UsageLeaderboardPeriod },
})
}
>
<SelectTrigger className="w-36">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(PERIOD_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
}
>
{leaderboard.isLoading ? (
<div className="space-y-2 p-4">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
</div>
) : leaderboard.isError ? (
<p className="p-4 text-xs text-destructive">
Failed to load usage data: {leaderboard.error.message}
</p>
) : !leaderboard.data?.rows.length ? (
<div className="p-6 text-center text-sm text-muted-foreground">
No Open SWE Agent usage has been recorded for{" "}
{PERIOD_LABELS[activePeriod].toLowerCase()} yet.
</div>
) : (
<UsageTable
rows={leaderboard.data.rows}
totalMembers={leaderboard.data.total_members}
/>
)}
</SettingsSection>
<SettingsSection
title="Reviewer stats"
description="Issues surfaced by Open SWE Review and how often users addressed them."
>
{leaderboard.isLoading ? (
<div className="grid gap-3 p-4 sm:grid-cols-2">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
) : leaderboard.isError ? (
<p className="p-4 text-xs text-destructive">
Failed to load reviewer stats: {leaderboard.error.message}
</p>
) : leaderboard.data?.reviewer_stats ? (
<ReviewerStats stats={leaderboard.data.reviewer_stats} />
) : (
<div className="p-6 text-center text-sm text-muted-foreground">
No reviewer stats have been recorded for{" "}
{PERIOD_LABELS[activePeriod].toLowerCase()} yet.
</div>
)}
</SettingsSection>
</AppShell>
)
}
function UsageTable({
rows,
totalMembers,
}: {
rows: Array<UsageLeaderboardRow>
totalMembers: number
}) {
return (
<div className="overflow-x-auto">
<table className="w-full min-w-[760px] text-sm">
<thead className="border-b border-border text-xs text-muted-foreground">
<tr>
<th className="w-14 px-4 py-3 text-left font-normal">Rank</th>
<th className="px-2 py-3 text-left font-normal">User</th>
<th className="px-2 py-3 text-left font-normal">Favorite Model</th>
<th className="px-2 py-3 text-right font-normal">Agent Runs</th>
<th className="px-2 py-3 text-right font-normal">PRs Opened</th>
<th className="px-2 py-3 text-right font-normal">Merged PRs</th>
<th className="px-4 py-3 text-right font-normal">Agent LOC</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{rows.map((row) => (
<tr
key={`${row.rank}-${row.user.github_login ?? row.user.email ?? row.user.name}`}
>
<td className="px-4 py-3 text-muted-foreground">{row.rank}</td>
<td className="px-2 py-3">
<UserCell row={row} />
</td>
<td className="max-w-48 truncate px-2 py-3 text-muted-foreground">
{row.favorite_model}
</td>
<td className="px-2 py-3 text-right tabular-nums">
{formatNumber(row.agent_runs)}
</td>
<td className="px-2 py-3 text-right tabular-nums">
{formatNumber(row.prs_opened)}
</td>
<td className="px-2 py-3 text-right tabular-nums">
{formatNumber(row.merged_prs)}
</td>
<td
className="px-4 py-3 text-right tabular-nums"
title={`${formatNumber(row.additions)} additions, ${formatNumber(row.deletions)} deletions`}
>
{formatNumber(row.agent_loc)}
</td>
</tr>
))}
</tbody>
</table>
<div className="border-t border-border px-4 py-3 text-xs text-muted-foreground">
Top {Math.min(10, totalMembers)} of {formatNumber(totalMembers)} member
{totalMembers === 1 ? "" : "s"}
</div>
</div>
)
}
function ReviewerStats({ stats }: { stats: ReviewerStatsPayload }) {
const cards = [
{
label: "Reviewed PRs",
value: stats.reviewed_prs,
helper: `${formatNumber(stats.prs_with_findings)} with findings`,
},
{
label: "Issues surfaced",
value: stats.surfaced_findings,
helper: `${formatNumber(stats.findings_recorded)} recorded`,
},
{
label: "Addressed & resolved",
value: stats.addressed_findings,
helper: `${formatPercent(stats.resolution_rate)} of surfaced`,
},
{
label: "Resolved after update",
value: stats.resolved_after_update,
helper: "Resolved on a later PR head",
},
{
label: "Awaiting follow-up",
value: stats.unresolved_surfaced_findings,
helper: "Surfaced but not resolved/dismissed",
},
{
label: "Dismissed",
value: stats.dismissed_findings,
helper: `${formatNumber(stats.human_replies)} human replies tracked`,
},
]
return (
<div className="space-y-4 p-4">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
{cards.map((card) => (
<div key={card.label} className="rounded-md border border-border p-3">
<div className="text-xs text-muted-foreground">{card.label}</div>
<div className="mt-1 text-xl font-medium tabular-nums">
{formatNumber(card.value)}
</div>
<div className="mt-1 text-xs text-muted-foreground">
{card.helper}
</div>
</div>
))}
</div>
<div className="grid gap-4 border-t border-border pt-4 sm:grid-cols-2">
<CounterList title="Top categories" rows={stats.top_categories} />
<CounterList title="Severity mix" rows={severityRows(stats)} />
</div>
</div>
)
}
function severityRows(
stats: ReviewerStatsPayload
): Array<{ name: string; count: number }> {
return ["critical", "high", "medium", "low"]
.map((severity) => ({
name: severity,
count: stats.severity_counts[severity] ?? 0,
}))
.filter((row) => row.count > 0)
}
function CounterList({
title,
rows,
}: {
title: string
rows: Array<{ name: string; count: number }>
}) {
return (
<div>
<h3 className="text-xs font-medium text-muted-foreground">{title}</h3>
{rows.length ? (
<ul className="mt-2 space-y-2 text-sm">
{rows.map((row) => (
<li
key={row.name}
className="flex items-center justify-between gap-3"
>
<span className="truncate text-foreground">{row.name}</span>
<span className="text-muted-foreground tabular-nums">
{formatNumber(row.count)}
</span>
</li>
))}
</ul>
) : (
<p className="mt-2 text-xs text-muted-foreground">No data yet.</p>
)}
</div>
)
}
function UserCell({ row }: { row: UsageLeaderboardRow }) {
const initials = initialsFor(row.user.name)
return (
<div className="flex min-w-0 items-center gap-2.5">
<Avatar>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col">
<span className="truncate font-medium text-foreground">
{row.user.name}
</span>
<span className="truncate text-xs text-muted-foreground">
{row.user.email ?? row.user.github_login ?? "unknown"}
</span>
</div>
</div>
)
}
function initialsFor(name: string): string {
const parts = name.trim().split(/\s+/).filter(Boolean)
if (!parts.length) return "?"
const first = parts[0] ?? "?"
const second = parts[1]
if (!second) return first.slice(0, 2).toUpperCase()
return `${first[0] ?? ""}${second[0] ?? ""}`.toUpperCase()
}
function formatNumber(value: number): string {
return new Intl.NumberFormat().format(value)
}
function formatPercent(value: number): string {
return new Intl.NumberFormat(undefined, {
maximumFractionDigits: 0,
style: "percent",
}).format(value)
}