Skip to content

Commit 21aa6a3

Browse files
authored
Merge pull request #55 from xebia/copilot/fix-54
[Feature]: Update power users list to show top 10%, make panel bigger, and add tooltip
2 parents 11ea619 + 216a742 commit 21aa6a3

File tree

3 files changed

+69
-50
lines changed

3 files changed

+69
-50
lines changed

src/App.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Separator } from "@/components/ui/separator";
1111
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";
1212
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
1313
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
14+
import { Tooltip as UITooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
1415
import { DeploymentFooter } from "@/components/DeploymentFooter";
1516
import {
1617
AggregatedData,
@@ -381,18 +382,21 @@ function App() {
381382
</span>
382383
</div>
383384
{powerUserSummary && (
384-
<Sheet>
385-
<SheetTrigger asChild>
386-
<Button variant="outline" className="flex items-center gap-2">
387-
<span className="text-sm">Power Users:</span>
388-
<span className="font-bold">{powerUserSummary.totalPowerUsers}</span>
389-
</Button>
390-
</SheetTrigger>
391-
<SheetContent className="w-[600px] sm:w-[800px] overflow-y-auto">
392-
<SheetHeader>
393-
<SheetTitle>Power Users Analysis</SheetTitle>
394-
</SheetHeader>
395-
<div className="mt-6 space-y-6">
385+
<UITooltip>
386+
<TooltipTrigger asChild>
387+
<Sheet>
388+
<SheetTrigger asChild>
389+
<Button variant="outline" className="flex items-center gap-2">
390+
<span className="text-sm">Power Users:</span>
391+
<span className="font-bold">{powerUserSummary.totalPowerUsers}</span>
392+
</Button>
393+
</SheetTrigger>
394+
<SheetContent side="bottom" className="h-[90vh] max-w-[90%] mx-auto overflow-y-auto">
395+
<div className="p-7">
396+
<SheetHeader>
397+
<SheetTitle className="text-xl">Power Users Analysis</SheetTitle>
398+
</SheetHeader>
399+
<div className="mt-6 space-y-6">
396400
{/* Power User Summary */}
397401
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
398402
<Card className="p-4">
@@ -508,9 +512,16 @@ function App() {
508512
</Table>
509513
</div>
510514
</Card>
511-
</div>
512-
</SheetContent>
513-
</Sheet>
515+
</div>
516+
</div>
517+
</SheetContent>
518+
</Sheet>
519+
</TooltipTrigger>
520+
<TooltipContent>
521+
<p>Power users are the top 10% of users by request count.<br/>
522+
These users make the most requests to GitHub Copilot.</p>
523+
</TooltipContent>
524+
</UITooltip>
514525
)}
515526
</div>
516527
</div>

src/lib/utils.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,7 @@ export interface PowerUserSummary {
232232
powerUserModelSummary: ModelUsageSummary[];
233233
}
234234

235-
// Define power user threshold - users with more than 10 requests
236-
export const POWER_USER_THRESHOLD = 10;
235+
237236

238237
export function getPowerUsers(data: CopilotUsageData[]): PowerUserSummary {
239238
// First, aggregate total requests per user
@@ -242,11 +241,17 @@ export function getPowerUsers(data: CopilotUsageData[]): PowerUserSummary {
242241
userTotals[item.user] = (userTotals[item.user] || 0) + item.requestsUsed;
243242
});
244243

245-
// Identify power users (users exceeding threshold)
246-
const powerUserNames = Object.keys(userTotals).filter(
247-
user => userTotals[user] > POWER_USER_THRESHOLD
244+
// Get all users sorted by total requests (descending)
245+
const allUsersSorted = Object.keys(userTotals).sort(
246+
(a, b) => userTotals[b] - userTotals[a]
248247
);
249248

249+
// Calculate top 10% of users (at least 1 user if any users exist)
250+
const powerUserCount = Math.max(1, Math.ceil(allUsersSorted.length * 0.1));
251+
252+
// Take the top 10% of users as power users
253+
const powerUserNames = allUsersSorted.slice(0, powerUserCount);
254+
250255
// Filter data to only power users
251256
const powerUserData = data.filter(item => powerUserNames.includes(item.user));
252257

src/test/power-users.test.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { getPowerUsers, getPowerUserDailyData, POWER_USER_THRESHOLD, CopilotUsageData } from '../lib/utils';
2+
import { getPowerUsers, getPowerUserDailyData, CopilotUsageData } from '../lib/utils';
33

44
describe('Power Users Functionality', () => {
55
const mockData: CopilotUsageData[] = [
@@ -56,20 +56,20 @@ describe('Power Users Functionality', () => {
5656
it('should identify power users correctly', () => {
5757
const result = getPowerUsers(mockData);
5858

59-
expect(result.totalPowerUsers).toBe(2);
60-
expect(result.powerUsers).toHaveLength(2);
59+
// With 4 users total, top 10% = Math.ceil(4 * 0.1) = 1 user
60+
expect(result.totalPowerUsers).toBe(1);
61+
expect(result.powerUsers).toHaveLength(1);
6162

62-
// Should be sorted by total requests (descending)
63+
// Should be sorted by total requests (descending) - only the top user
6364
expect(result.powerUsers[0].user).toBe('power-user-1');
6465
expect(result.powerUsers[0].totalRequests).toBe(35); // 15 + 8 + 12
65-
expect(result.powerUsers[1].user).toBe('power-user-2');
66-
expect(result.powerUsers[1].totalRequests).toBe(20);
6766
});
6867

6968
it('should calculate total power user requests correctly', () => {
7069
const result = getPowerUsers(mockData);
7170

72-
expect(result.totalPowerUserRequests).toBe(55); // 35 + 20
71+
// Only power-user-1 is a power user now
72+
expect(result.totalPowerUserRequests).toBe(35); // Only power-user-1
7373
});
7474

7575
it('should aggregate requests by model for power users', () => {
@@ -81,10 +81,9 @@ describe('Power Users Functionality', () => {
8181
'gpt-3.5': 8
8282
});
8383

84+
// power-user-2 is no longer a power user (only top 10% = 1 user)
8485
const powerUser2 = result.powerUsers.find(u => u.user === 'power-user-2');
85-
expect(powerUser2?.requestsByModel).toEqual({
86-
'claude': 20
87-
});
86+
expect(powerUser2).toBeUndefined();
8887
});
8988

9089
it('should calculate daily activity for power users', () => {
@@ -100,19 +99,21 @@ describe('Power Users Functionality', () => {
10099
it('should create model usage summary for power users only', () => {
101100
const result = getPowerUsers(mockData);
102101

103-
expect(result.powerUserModelSummary).toHaveLength(3);
102+
// Only power-user-1 is a power user, so only models they used should be included
103+
expect(result.powerUserModelSummary).toHaveLength(2);
104104

105-
// Should include models used by power users
105+
// Should include models used by power users (only power-user-1)
106106
const gpt4Summary = result.powerUserModelSummary.find(m => m.model === 'gpt-4');
107107
expect(gpt4Summary?.totalRequests).toBe(27);
108108
expect(gpt4Summary?.compliantRequests).toBe(15);
109109
expect(gpt4Summary?.exceedingRequests).toBe(12);
110110

111-
const claudeSummary = result.powerUserModelSummary.find(m => m.model === 'claude');
112-
expect(claudeSummary?.totalRequests).toBe(20);
113-
114111
const gpt35Summary = result.powerUserModelSummary.find(m => m.model === 'gpt-3.5');
115112
expect(gpt35Summary?.totalRequests).toBe(8);
113+
114+
// claude should not be included since power-user-2 is not a power user anymore
115+
const claudeSummary = result.powerUserModelSummary.find(m => m.model === 'claude');
116+
expect(claudeSummary).toBeUndefined();
116117
});
117118

118119
it('should handle case with no power users', () => {
@@ -137,38 +138,40 @@ describe('Power Users Functionality', () => {
137138

138139
const result = getPowerUsers(lowUsageData);
139140

140-
expect(result.totalPowerUsers).toBe(0);
141-
expect(result.powerUsers).toHaveLength(0);
142-
expect(result.totalPowerUserRequests).toBe(0);
143-
expect(result.powerUserModelSummary).toHaveLength(0);
141+
// With 2 users, top 10% = Math.ceil(2 * 0.1) = 1 user, so user-1 will be the power user
142+
expect(result.totalPowerUsers).toBe(1);
143+
expect(result.powerUsers).toHaveLength(1);
144+
expect(result.powerUsers[0].user).toBe('user-1'); // user with highest requests (5)
145+
expect(result.totalPowerUserRequests).toBe(5);
146+
expect(result.powerUserModelSummary).toHaveLength(1);
144147
});
145148

146149
it('should generate daily data aggregated across all power users', () => {
147150
const result = getPowerUsers(mockData);
148151
const dailyData = getPowerUserDailyData(result.powerUsers);
149152

153+
// Only power-user-1 is a power user now
150154
expect(dailyData).toEqual([
151-
{ date: '2025-01-01', requests: 43 }, // power-user-1: 23 + power-user-2: 20
152-
{ date: '2025-01-02', requests: 12 } // power-user-1: 12
155+
{ date: '2025-01-01', requests: 23 }, // power-user-1 only: 23
156+
{ date: '2025-01-02', requests: 12 } // power-user-1 only: 12
153157
]);
154158
});
155159

156-
it('should use correct power user threshold', () => {
157-
expect(POWER_USER_THRESHOLD).toBe(10);
158-
159-
// Test with user exactly at threshold
160-
const thresholdData: CopilotUsageData[] = [
160+
it('should use top 10% logic for power users', () => {
161+
// Test with single user - should always have 1 power user
162+
const singleUserData: CopilotUsageData[] = [
161163
{
162164
timestamp: new Date('2025-01-01T10:00:00Z'),
163-
user: 'threshold-user',
165+
user: 'only-user',
164166
model: 'gpt-4',
165-
requestsUsed: 10,
167+
requestsUsed: 1,
166168
exceedsQuota: false,
167169
totalMonthlyQuota: '100'
168170
}
169171
];
170172

171-
const result = getPowerUsers(thresholdData);
172-
expect(result.totalPowerUsers).toBe(0); // Should not be a power user (needs > threshold)
173+
const result = getPowerUsers(singleUserData);
174+
expect(result.totalPowerUsers).toBe(1); // Should have 1 power user (top 10% of 1 user = 1)
175+
expect(result.powerUsers[0].user).toBe('only-user');
173176
});
174177
});

0 commit comments

Comments
 (0)