Skip to content

Commit 238d52a

Browse files
rajbosCopilot
andauthored
feat: add exceeded users overview panel (closes #82) (#141)
Show a clickable 'Users Exceeding Quota' stat in the summary bar that opens a dialog listing all users who have exceeded their quota, including: - Days with exceeded requests - Total exceeded requests - Compliant (other) requests made on those same days - Total requests on exceeded days - Worst day (date + exceeded vs total) Sorted by total exceeded requests descending so the biggest offenders appear first. Summary cards show totals across all users at the top. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6ebb27e commit 238d52a

2 files changed

Lines changed: 154 additions & 3 deletions

File tree

src/App.tsx

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/
1313
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table";
1414
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
1515
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
16-
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
16+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
1717
import { DeploymentFooter } from "@/components/DeploymentFooter";
1818
import {
1919
AggregatedData,
@@ -23,6 +23,7 @@ import {
2323
PowerUserSummary,
2424
PowerUserDailyBreakdown,
2525
ExceededRequestDetail,
26+
ExceededUserSummary,
2627
ProjectedUserData,
2728
MonthOption,
2829
UserAnalysisData,
@@ -40,6 +41,7 @@ import {
4041
getExceededRequestDetails,
4142
getUserExceededRequestSummary,
4243
getUniqueUsersExceedingQuota,
44+
getExceededUsersOverview,
4345
getTotalRequestsForUsersExceedingQuota,
4446
getProjectedUsersExceedingQuota,
4547
getProjectedUsersExceedingQuotaDetails,
@@ -502,6 +504,8 @@ function App() {
502504
const [showPotentialCostDetails, setShowPotentialCostDetails] = useState(false);
503505
const [showProjectedUsersDialog, setShowProjectedUsersDialog] = useState(false);
504506
const [projectedUsersData, setProjectedUsersData] = useState<ProjectedUserData[]>([]);
507+
const [showExceededUsersOverview, setShowExceededUsersOverview] = useState(false);
508+
const [exceededUsersOverviewData, setExceededUsersOverviewData] = useState<ExceededUserSummary[]>([]);
505509
const [selectedSearchUser, setSelectedSearchUser] = useState<string | null>(null);
506510
const [userAnalysisData, setUserAnalysisData] = useState<UserAnalysisData | null>(null);
507511
const [expectedExcessCost, setExpectedExcessCost] = useState<number>(0);
@@ -576,6 +580,9 @@ function App() {
576580
const exceedingUsersCount = getUniqueUsersExceedingQuota(displayData, selectedPlan);
577581
setUsersExceedingQuota(exceedingUsersCount);
578582

583+
// Compute exceeded users overview for the overview dialog
584+
setExceededUsersOverviewData(getExceededUsersOverview(displayData));
585+
579586
// Get projected count of users who will exceed quota by month-end for display data
580587
const projectedExceedingUsersCount = getProjectedUsersExceedingQuota(displayData, selectedPlan);
581588
setProjectedUsersExceedingQuota(projectedExceedingUsersCount);
@@ -628,6 +635,9 @@ function App() {
628635
const exceedingUsersCount = getUniqueUsersExceedingQuota(filteredData, selectedPlan);
629636
setUsersExceedingQuota(exceedingUsersCount);
630637

638+
// Compute exceeded users overview for the overview dialog
639+
setExceededUsersOverviewData(getExceededUsersOverview(filteredData));
640+
631641
// Get projected count of users who will exceed quota by month-end for the selected month
632642
const projectedExceedingUsersCount = getProjectedUsersExceedingQuota(filteredData, selectedPlan);
633643
setProjectedUsersExceedingQuota(projectedExceedingUsersCount);
@@ -1355,11 +1365,16 @@ function App() {
13551365
{uniqueModels.length}
13561366
</span>
13571367
</div>
1358-
<div className="flex items-center gap-2">
1368+
<div
1369+
className={usersExceedingQuota > 0 ? "xebia-action-button" : "flex items-center gap-2"}
1370+
onClick={usersExceedingQuota > 0 ? () => setShowExceededUsersOverview(true) : undefined}
1371+
title={usersExceedingQuota > 0 ? "Click to see which users exceeded their quota" : undefined}
1372+
>
13591373
<span className="text-sm text-muted-foreground">Users Exceeding Quota:</span>
1360-
<span className="text-lg font-bold text-red-600">
1374+
<span className={`${usersExceedingQuota > 0 ? "value text-red-600" : "text-lg font-bold text-green-600"}`}>
13611375
{usersExceedingQuota.toLocaleString()}
13621376
</span>
1377+
{usersExceedingQuota > 0 && <ChevronRight className="icon" />}
13631378
</div>
13641379
<div
13651380
className="xebia-action-button"
@@ -2094,6 +2109,95 @@ function App() {
20942109
</div>
20952110
)}
20962111

2112+
{/* Exceeded Users Overview Dialog */}
2113+
<Dialog open={showExceededUsersOverview} onOpenChange={setShowExceededUsersOverview}>
2114+
<DialogContent className="w-[98vw] max-w-none max-h-[85vh] overflow-y-auto" style={{ width: '98vw', maxWidth: 'none' }}>
2115+
<DialogHeader>
2116+
<DialogTitle>Users Exceeding Quota</DialogTitle>
2117+
<DialogDescription>
2118+
All users who have exceeded their quota at least once — including how often, how many exceeded requests they made, and how many other requests they made on the same days.
2119+
</DialogDescription>
2120+
</DialogHeader>
2121+
<div className="space-y-4">
2122+
{exceededUsersOverviewData.length > 0 ? (
2123+
<>
2124+
{/* Summary row */}
2125+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
2126+
<Card className="p-4">
2127+
<div className="text-sm text-muted-foreground">Users Exceeded</div>
2128+
<div className="text-2xl font-bold text-red-600">{exceededUsersOverviewData.length.toLocaleString()}</div>
2129+
</Card>
2130+
<Card className="p-4">
2131+
<div className="text-sm text-muted-foreground">Total Exceeded Requests</div>
2132+
<div className="text-2xl font-bold text-red-600">
2133+
{exceededUsersOverviewData.reduce((s, u) => s + u.totalExceededRequests, 0).toLocaleString()}
2134+
</div>
2135+
</Card>
2136+
<Card className="p-4">
2137+
<div className="text-sm text-muted-foreground">Total Days with Exceeded Requests</div>
2138+
<div className="text-2xl font-bold">
2139+
{exceededUsersOverviewData.reduce((s, u) => s + u.daysExceeded, 0).toLocaleString()}
2140+
</div>
2141+
</Card>
2142+
<Card className="p-4">
2143+
<div className="text-sm text-muted-foreground">Avg Days per User</div>
2144+
<div className="text-2xl font-bold">
2145+
{(exceededUsersOverviewData.reduce((s, u) => s + u.daysExceeded, 0) / exceededUsersOverviewData.length).toFixed(1)}
2146+
</div>
2147+
</Card>
2148+
</div>
2149+
2150+
{/* Per-user table */}
2151+
<Card className="p-4">
2152+
<h3 className="text-md font-medium mb-3">Per-User Breakdown</h3>
2153+
<div className="overflow-auto">
2154+
<Table>
2155+
<TableHeader>
2156+
<TableRow>
2157+
<TableHead className="min-w-[160px]">User</TableHead>
2158+
<TableHead className="text-right min-w-[110px]">Days Exceeded</TableHead>
2159+
<TableHead className="text-right min-w-[160px]">Total Exceeded Requests</TableHead>
2160+
<TableHead className="text-right min-w-[160px]">Other Requests (Same Days)</TableHead>
2161+
<TableHead className="text-right min-w-[160px]">Total Requests (Same Days)</TableHead>
2162+
<TableHead className="min-w-[130px]">Worst Day</TableHead>
2163+
</TableRow>
2164+
</TableHeader>
2165+
<TableBody>
2166+
{exceededUsersOverviewData.map((row) => (
2167+
<TableRow key={row.user}>
2168+
<TableCell className="font-medium">{row.user}</TableCell>
2169+
<TableCell className="text-right">{row.daysExceeded.toLocaleString()}</TableCell>
2170+
<TableCell className="text-right text-red-600 font-medium">
2171+
{row.totalExceededRequests.toLocaleString()}
2172+
</TableCell>
2173+
<TableCell className="text-right">
2174+
{row.compliantRequestsOnExceededDays.toLocaleString()}
2175+
</TableCell>
2176+
<TableCell className="text-right font-medium">
2177+
{row.totalRequestsOnExceededDays.toLocaleString()}
2178+
</TableCell>
2179+
<TableCell className="text-sm">
2180+
<div className="font-medium">{row.worstDay.date}</div>
2181+
<div className="text-muted-foreground text-xs">
2182+
{row.worstDay.exceededRequests.toLocaleString()} exceeded of {row.worstDay.totalRequests.toLocaleString()} total
2183+
</div>
2184+
</TableCell>
2185+
</TableRow>
2186+
))}
2187+
</TableBody>
2188+
</Table>
2189+
</div>
2190+
</Card>
2191+
</>
2192+
) : (
2193+
<Card className="p-8 text-center">
2194+
<p className="text-muted-foreground">No users have exceeded their quota in the selected period. 🎉</p>
2195+
</Card>
2196+
)}
2197+
</div>
2198+
</DialogContent>
2199+
</Dialog>
2200+
20972201
{/* Exceeded Request Details Dialog */}
20982202
<Dialog open={showExceededDetails} onOpenChange={setShowExceededDetails}>
20992203
<DialogContent className="w-[98vw] max-w-none max-h-[85vh] overflow-y-auto" style={{ width: '98vw', maxWidth: 'none' }}>

src/lib/utils.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,53 @@ export function getUserExceededRequestSummary(data: CopilotUsageData[], userName
671671
};
672672
}
673673

674+
export interface ExceededUserSummary {
675+
user: string;
676+
daysExceeded: number;
677+
totalExceededRequests: number;
678+
totalRequestsOnExceededDays: number;
679+
compliantRequestsOnExceededDays: number;
680+
worstDay: { date: string; exceededRequests: number; totalRequests: number };
681+
}
682+
683+
/**
684+
* Get a per-user overview of all users who have exceeded their quota at least once.
685+
* Sorted by total exceeded requests descending.
686+
*/
687+
export function getExceededUsersOverview(data: CopilotUsageData[]): ExceededUserSummary[] {
688+
const allDetails = getExceededRequestDetails(data);
689+
if (allDetails.length === 0) return [];
690+
691+
const byUser: Record<string, ExceededUserSummary> = {};
692+
693+
allDetails.forEach(detail => {
694+
if (!byUser[detail.user]) {
695+
byUser[detail.user] = {
696+
user: detail.user,
697+
daysExceeded: 0,
698+
totalExceededRequests: 0,
699+
totalRequestsOnExceededDays: 0,
700+
compliantRequestsOnExceededDays: 0,
701+
worstDay: { date: detail.date, exceededRequests: 0, totalRequests: 0 },
702+
};
703+
}
704+
const summary = byUser[detail.user];
705+
summary.daysExceeded += 1;
706+
summary.totalExceededRequests += detail.exceededRequests;
707+
summary.totalRequestsOnExceededDays += detail.totalRequestsOnDay;
708+
summary.compliantRequestsOnExceededDays += detail.compliantRequestsOnDay;
709+
if (detail.exceededRequests > summary.worstDay.exceededRequests) {
710+
summary.worstDay = {
711+
date: detail.date,
712+
exceededRequests: detail.exceededRequests,
713+
totalRequests: detail.totalRequestsOnDay,
714+
};
715+
}
716+
});
717+
718+
return Object.values(byUser).sort((a, b) => b.totalExceededRequests - a.totalExceededRequests);
719+
}
720+
674721
/**
675722
* Get the count of unique users who have exceeded their quota limits
676723
* @param data - Array of Copilot usage data

0 commit comments

Comments
 (0)