Skip to content

Commit 8a1537f

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/npm-dependencies-75b6730bc5
2 parents 8c8eaab + c1e618b commit 8a1537f

7 files changed

Lines changed: 114 additions & 70 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
"features": {
2525
"ghcr.io/devcontainers/features/sshd:1": {
2626
"version": "latest"
27+
},
28+
"ghcr.io/devcontainers/features/github-cli:1": {
29+
"version": "latest"
30+
},
31+
"ghcr.io/devcontainers/features/azure-cli:1": {
32+
"version": "latest"
2733
}
2834
}
2935
}

.devcontainer/onCreate.sh

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,20 @@
22

33
set -e
44

5-
echo "Installing the GitHub CLI"
6-
(type -p wget >/dev/null || (sudo apt update && sudo apt-get install wget -y)) \
7-
&& sudo mkdir -p -m 755 /etc/apt/keyrings \
8-
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
9-
&& cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
10-
&& sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
11-
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
12-
&& sudo apt update \
13-
&& sudo apt install gh inotify-tools -y
5+
echo "Setting up node user home directory and permissions"
6+
sudo mkdir -p /home/node/.config/vscode-dev-containers
7+
sudo chown -R node:node /home/node
8+
sudo chmod 755 /home/node
9+
sudo chmod -R 755 /home/node/.config
1410

15-
echo "Installing azcopy"
16-
17-
sudo wget -O /usr/local/bin/azcopytar https://aka.ms/downloadazcopy-v10-linux
18-
sudo tar -xvf /usr/local/bin/azcopytar -C /usr/local/bin/
19-
sudo rm /usr/local/bin/azcopytar
20-
azcopy_dir=$(find /usr/local/bin/ -type d -name "azcopy*" | head -n 1)
21-
sudo mv "$azcopy_dir/azcopy" /usr/local/bin/azcopy
22-
sudo rm -rf "$azcopy_dir"
11+
echo "Installing inotify-tools"
12+
sudo apt update && sudo apt install -y inotify-tools
2313

2414
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
2515
"$SCRIPT_DIR/refreshTools.sh"
2616

27-
echo "Pre-starting the server and generating the optimized assets"
28-
npm run optimize --override
17+
echo "Installing Node.js dependencies"
18+
npm install
2919

3020
echo "Installing supervisor"
3121
sudo apt-get update && sudo apt-get install -y supervisor

.devcontainer/refreshTools.sh

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ set -e
44

55
echo "Checking for updates to workbench-template from GitHub"
66

7+
# Ensure jq is installed for JSON parsing
8+
if ! command -v jq &> /dev/null; then
9+
echo "Installing jq for JSON parsing..."
10+
sudo apt-get update && sudo apt-get install -y jq
11+
fi
12+
713
WORKSPACE_DIR="/workspaces/spark-template"
814

915
MARKER_DIR="/var/lib/spark/.versions"
@@ -12,11 +18,28 @@ TOOLS_MARKER_FILE="$MARKER_DIR/tools"
1218

1319
sudo mkdir -p "$MARKER_DIR"
1420

21+
# Check if TEMPLATE_PAT is set
22+
if [ -z "$TEMPLATE_PAT" ]; then
23+
echo "TEMPLATE_PAT environment variable is not set. Skipping tools update."
24+
exit 0
25+
fi
26+
1527
# Fetch the latest release information
1628
LATEST_RELEASE=$(curl -s -H "Authorization: token $TEMPLATE_PAT" https://api.github.com/repos/github/spark-template/releases/latest)
1729

30+
# Check if the API call succeeded
31+
if [ $? -ne 0 ] || [ -z "$LATEST_RELEASE" ]; then
32+
echo "Failed to fetch release information from GitHub. Skipping tools update."
33+
exit 0
34+
fi
35+
1836
# Check if marker file exists and has the same release ID
1937
RELEASE_ID=$(echo "$LATEST_RELEASE" | jq -r '.id')
38+
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
39+
echo "Invalid release data received from GitHub. Skipping tools update."
40+
exit 0
41+
fi
42+
2043
if [ -f "$RELEASE_MARKER_FILE" ] && [ "$(cat "$RELEASE_MARKER_FILE")" == "$RELEASE_ID" ]; then
2144
echo "Already at the latest release. Skipping download."
2245
exit 0
@@ -28,6 +51,13 @@ TEMP_DIR=$(mktemp -d)
2851
cd $TEMP_DIR
2952

3053
DOWNLOAD_URL=$(echo "$LATEST_RELEASE" | jq -r '.assets[0].url')
54+
if [ "$DOWNLOAD_URL" == "null" ] || [ -z "$DOWNLOAD_URL" ]; then
55+
echo "No download URL found in release data. Skipping tools update."
56+
cd - >/dev/null
57+
rm -rf $TEMP_DIR
58+
exit 0
59+
fi
60+
3161
curl -L -o dist.zip -H "Authorization: token $TEMPLATE_PAT" -H "Accept: application/octet-stream" "$DOWNLOAD_URL"
3262

3363
unzip -o dist.zip

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"lint": "eslint .",
1212
"test": "vitest",
1313
"test:run": "vitest run",
14-
"optimize": "vite optimize",
1514
"preview": "vite preview",
1615
"build:pages": "bash ./build-for-gh-pages.sh",
1716
"predeploy": "npm run build:pages",

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)