Skip to content

Commit 577f0e5

Browse files
authored
Merge pull request #98 from rajbos/main
Add plan-aware user quota tracking to top bar statistics (#26)
2 parents 0178a32 + 617cfcb commit 577f0e5

3 files changed

Lines changed: 242 additions & 2 deletions

File tree

src/App.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState, useCallback, useRef, DragEvent } from "react";
1+
import React, { useState, useCallback, useRef, DragEvent, useEffect } from "react";
22
import { Upload, GithubLogo, CircleNotch } from "@phosphor-icons/react";
33
import { toast, Toaster } from "sonner";
44
import {
@@ -34,7 +34,8 @@ import {
3434
getUniqueModelsFromBreakdown,
3535
getLastDateFromData,
3636
getExceededRequestDetails,
37-
getUserExceededRequestSummary
37+
getUserExceededRequestSummary,
38+
getUniqueUsersExceedingQuota
3839
} from "@/lib/utils";
3940

4041
function App() {
@@ -54,8 +55,17 @@ function App() {
5455
const [showExceededDetails, setShowExceededDetails] = useState(false);
5556
const [exceededDetailsData, setExceededDetailsData] = useState<ExceededRequestDetail[]>([]);
5657
const [selectedDate, setSelectedDate] = useState<string | null>(null);
58+
const [usersExceedingQuota, setUsersExceedingQuota] = useState<number>(0);
5759
const fileInputRef = useRef<HTMLInputElement>(null);
5860

61+
// Recalculate users exceeding quota when plan selection changes
62+
useEffect(() => {
63+
if (data && data.length > 0) {
64+
const exceedingUsersCount = getUniqueUsersExceedingQuota(data, selectedPlan);
65+
setUsersExceedingQuota(exceedingUsersCount);
66+
}
67+
}, [selectedPlan, data]);
68+
5969
const handlePowerUserSelect = useCallback((userName: string | null) => {
6070
setSelectedPowerUser(userName);
6171
}, []);
@@ -137,6 +147,10 @@ function App() {
137147
const powerUserBreakdown = getPowerUserDailyBreakdown(parsedData, powerUserNames);
138148
setPowerUserDailyBreakdown(powerUserBreakdown);
139149

150+
// Get count of users exceeding quota for top bar display
151+
const exceedingUsersCount = getUniqueUsersExceedingQuota(parsedData, selectedPlan);
152+
setUsersExceedingQuota(exceedingUsersCount);
153+
140154
// Get the last date available in the CSV
141155
const lastDate = getLastDateFromData(parsedData);
142156
setLastDateAvailable(lastDate);
@@ -185,6 +199,7 @@ function App() {
185199
setPowerUserSummary(null);
186200
setPowerUserDailyBreakdown([]);
187201
setSelectedPowerUser(null);
202+
setUsersExceedingQuota(0);
188203
setLastDateAvailable(null);
189204
}
190205
};
@@ -513,6 +528,12 @@ function App() {
513528
{uniqueModels.length}
514529
</span>
515530
</div>
531+
<div className="flex items-center gap-2">
532+
<span className="text-sm text-muted-foreground">Users Exceeding Quota:</span>
533+
<span className="text-lg font-bold text-red-600">
534+
{usersExceedingQuota.toLocaleString()}
535+
</span>
536+
</div>
516537
{powerUserSummary && (
517538
<UITooltip>
518539
<TooltipTrigger asChild>

src/lib/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,3 +595,26 @@ export function getUserExceededRequestSummary(data: CopilotUsageData[], userName
595595
},
596596
};
597597
}
598+
599+
/**
600+
* Get the count of unique users who have exceeded their quota limits
601+
* @param data - Array of Copilot usage data
602+
* @param plan - The plan type to check limits against (defaults to BUSINESS)
603+
*/
604+
export function getUniqueUsersExceedingQuota(data: CopilotUsageData[], plan: string = COPILOT_PLANS.BUSINESS): number {
605+
if (!data.length) return 0;
606+
607+
// Get the plan limit for comparison
608+
const planLimit = PLAN_MONTHLY_LIMITS[plan] || PLAN_MONTHLY_LIMITS[COPILOT_PLANS.BUSINESS];
609+
610+
// Aggregate total requests per user
611+
const userTotals: Record<string, number> = {};
612+
data.forEach(item => {
613+
userTotals[item.user] = (userTotals[item.user] || 0) + item.requestsUsed;
614+
});
615+
616+
// Count users who exceed the plan limit
617+
const usersExceedingPlan = Object.keys(userTotals).filter(user => userTotals[user] > planLimit);
618+
619+
return usersExceedingPlan.length;
620+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getUniqueUsersExceedingQuota, COPILOT_PLANS } from '@/lib/utils';
3+
import type { CopilotUsageData } from '@/lib/utils';
4+
5+
describe('Users Exceeding Quota', () => {
6+
const mockData: CopilotUsageData[] = [
7+
{
8+
timestamp: new Date('2025-01-01T10:00:00Z'),
9+
user: 'user1', // 10 + 5 + 3 = 18 total requests (under all plan limits)
10+
model: 'gpt-4o-2024-11-20',
11+
requestsUsed: 10,
12+
exceedsQuota: false,
13+
totalMonthlyQuota: '50'
14+
},
15+
{
16+
timestamp: new Date('2025-01-01T11:00:00Z'),
17+
user: 'user1',
18+
model: 'gpt-4o-2024-11-20',
19+
requestsUsed: 5,
20+
exceedsQuota: true, // This is irrelevant now - we count total usage
21+
totalMonthlyQuota: '50'
22+
},
23+
{
24+
timestamp: new Date('2025-01-01T12:00:00Z'),
25+
user: 'user2', // 20 + 15 = 35 total requests (under Individual plan limit of 50)
26+
model: 'gpt-4.1-2025-04-14',
27+
requestsUsed: 20,
28+
exceedsQuota: true, // This is irrelevant now
29+
totalMonthlyQuota: '50'
30+
},
31+
{
32+
timestamp: new Date('2025-01-02T10:00:00Z'),
33+
user: 'user1',
34+
model: 'gpt-4o-2024-11-20',
35+
requestsUsed: 3,
36+
exceedsQuota: true, // This is irrelevant now
37+
totalMonthlyQuota: '50'
38+
},
39+
{
40+
timestamp: new Date('2025-01-01T15:00:00Z'),
41+
user: 'user3', // 8 + 20 = 28 total requests (under Individual plan limit of 50)
42+
model: 'claude-3',
43+
requestsUsed: 8,
44+
exceedsQuota: false,
45+
totalMonthlyQuota: '50'
46+
},
47+
{
48+
timestamp: new Date('2025-01-02T11:00:00Z'),
49+
user: 'user2',
50+
model: 'gpt-4o-2024-11-20',
51+
requestsUsed: 15,
52+
exceedsQuota: false,
53+
totalMonthlyQuota: '50'
54+
},
55+
{
56+
timestamp: new Date('2025-01-03T11:00:00Z'),
57+
user: 'user3',
58+
model: 'gpt-4o-2024-11-20',
59+
requestsUsed: 20,
60+
exceedsQuota: false,
61+
totalMonthlyQuota: '50'
62+
},
63+
{
64+
timestamp: new Date('2025-01-04T11:00:00Z'),
65+
user: 'user4', // 60 total requests (exceeds Individual plan limit of 50)
66+
model: 'gpt-4o-2024-11-20',
67+
requestsUsed: 60,
68+
exceedsQuota: false, // exceedsQuota flag is irrelevant now
69+
totalMonthlyQuota: '50'
70+
}
71+
];
72+
73+
it('should count users who exceed Individual plan limit (50 requests)', () => {
74+
const result = getUniqueUsersExceedingQuota(mockData, COPILOT_PLANS.INDIVIDUAL);
75+
76+
// user1: 18 requests (under limit)
77+
// user2: 35 requests (under limit)
78+
// user3: 28 requests (under limit)
79+
// user4: 60 requests (exceeds 50 limit)
80+
// So only 1 user (user4) exceeds Individual plan limit
81+
expect(result).toBe(1);
82+
});
83+
84+
it('should count users who exceed Business plan limit (300 requests)', () => {
85+
const result = getUniqueUsersExceedingQuota(mockData, COPILOT_PLANS.BUSINESS);
86+
87+
// user1: 18 requests (under 300 limit)
88+
// user2: 35 requests (under 300 limit)
89+
// user3: 28 requests (under 300 limit)
90+
// user4: 60 requests (under 300 limit)
91+
// No users exceed Business plan limit of 300
92+
expect(result).toBe(0);
93+
});
94+
95+
it('should default to Business plan when no plan specified', () => {
96+
const result = getUniqueUsersExceedingQuota(mockData);
97+
98+
// Should use Business plan limit (300) by default
99+
// No users have >300 requests, so result should be 0
100+
expect(result).toBe(0);
101+
});
102+
103+
it('should count users who exceed Enterprise plan limit (1000 requests)', () => {
104+
const heavyUsageData: CopilotUsageData[] = [
105+
{
106+
timestamp: new Date('2025-01-01T10:00:00Z'),
107+
user: 'heavy-user1',
108+
model: 'gpt-4o-2024-11-20',
109+
requestsUsed: 1500, // Exceeds Enterprise limit of 1000
110+
exceedsQuota: false,
111+
totalMonthlyQuota: '1000'
112+
},
113+
{
114+
timestamp: new Date('2025-01-01T10:00:00Z'),
115+
user: 'normal-user',
116+
model: 'gpt-4o-2024-11-20',
117+
requestsUsed: 500, // Under Enterprise limit
118+
exceedsQuota: false,
119+
totalMonthlyQuota: '1000'
120+
}
121+
];
122+
123+
const result = getUniqueUsersExceedingQuota(heavyUsageData, COPILOT_PLANS.ENTERPRISE);
124+
125+
// heavy-user1: 1500 requests (exceeds 1000 limit)
126+
// normal-user: 500 requests (under 1000 limit)
127+
// So 1 user exceeds Enterprise plan limit
128+
expect(result).toBe(1);
129+
});
130+
131+
it('should return 0 when no users exceed quota', () => {
132+
const lightUsageData: CopilotUsageData[] = [
133+
{
134+
timestamp: new Date('2025-01-01T10:00:00Z'),
135+
user: 'user1',
136+
model: 'gpt-4o-2024-11-20',
137+
requestsUsed: 10,
138+
exceedsQuota: false,
139+
totalMonthlyQuota: '50'
140+
},
141+
{
142+
timestamp: new Date('2025-01-01T11:00:00Z'),
143+
user: 'user2',
144+
model: 'gpt-4.1-2025-04-14',
145+
requestsUsed: 5,
146+
exceedsQuota: false,
147+
totalMonthlyQuota: '50'
148+
}
149+
];
150+
151+
const result = getUniqueUsersExceedingQuota(lightUsageData, COPILOT_PLANS.INDIVIDUAL);
152+
153+
// user1: 10 requests (under 50 limit)
154+
// user2: 5 requests (under 50 limit)
155+
expect(result).toBe(0);
156+
});
157+
158+
it('should return 0 for empty data', () => {
159+
const result = getUniqueUsersExceedingQuota([]);
160+
expect(result).toBe(0);
161+
});
162+
163+
it('should aggregate multiple requests per user correctly', () => {
164+
const multipleRequestsData: CopilotUsageData[] = [
165+
{
166+
timestamp: new Date('2025-01-01T10:00:00Z'),
167+
user: 'power-user',
168+
model: 'gpt-4o-2024-11-20',
169+
requestsUsed: 25,
170+
exceedsQuota: false,
171+
totalMonthlyQuota: '50'
172+
},
173+
{
174+
timestamp: new Date('2025-01-02T10:00:00Z'),
175+
user: 'power-user',
176+
model: 'gpt-4o-2024-11-20',
177+
requestsUsed: 20,
178+
exceedsQuota: false,
179+
totalMonthlyQuota: '50'
180+
},
181+
{
182+
timestamp: new Date('2025-01-03T10:00:00Z'),
183+
user: 'power-user',
184+
model: 'gpt-4.1-2025-04-14',
185+
requestsUsed: 10,
186+
exceedsQuota: false,
187+
totalMonthlyQuota: '50'
188+
}
189+
];
190+
191+
const result = getUniqueUsersExceedingQuota(multipleRequestsData, COPILOT_PLANS.INDIVIDUAL);
192+
193+
// power-user: 25 + 20 + 10 = 55 total requests (exceeds 50 limit)
194+
expect(result).toBe(1);
195+
});
196+
});

0 commit comments

Comments
 (0)