Skip to content

Commit dc5c934

Browse files
authored
Merge branch 'main' into copilot/fix-69
2 parents 7659d8b + 21aa6a3 commit dc5c934

File tree

12 files changed

+381
-50
lines changed

12 files changed

+381
-50
lines changed

build-for-pages.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ npm install
117117

118118
# Build the app
119119
echo "Building application..."
120+
# Set deploy time for build
121+
export DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
122+
echo "Setting deploy time: $DEPLOY_TIME"
120123
npm run build
121124

122125
# Move the built files to the output directory

deploy-to-pages.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ cat > temp-vite.config.ts <<EOF
3030
$(cat vite.config.ts | sed "s/build: {/build: {\n base: '\/${REPO_NAME}\/',/")
3131
EOF
3232

33+
# Set deploy time for build
34+
export DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
35+
echo "Setting deploy time: $DEPLOY_TIME"
36+
3337
# Build with temporary config
3438
mv temp-vite.config.ts vite.config.ts
3539
npm run build

deploy.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ rm -rf dist
1010

1111
# Build the frontend
1212
echo "Compiling frontend..."
13+
# Set deploy time for build
14+
export DEPLOY_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
15+
echo "Setting deploy time: $DEPLOY_TIME"
16+
1317
npm install -f # force because there is a known mismatch of shadcn and react 19 - https://ui.shadcn.com/docs/react-19
1418
npm run build
1519

scripts/capture-deploy-info.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
3+
# Capture deployment information
4+
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
5+
BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
6+
DEPLOY_TIME=${DEPLOY_TIME:-$BUILD_TIME}
7+
8+
# Export as environment variables for Vite
9+
export VITE_GIT_BRANCH="$BRANCH_NAME"
10+
export VITE_BUILD_TIME="$BUILD_TIME"
11+
export VITE_DEPLOY_TIME="$DEPLOY_TIME"
12+
13+
echo "Deployment Info:"
14+
echo " Branch: $BRANCH_NAME"
15+
echo " Build Time: $BUILD_TIME"
16+
echo " Deploy Time: $DEPLOY_TIME"

src/App.tsx

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ 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";
15+
import { DeploymentFooter } from "@/components/DeploymentFooter";
1416
import {
1517
AggregatedData,
1618
CopilotUsageData,
@@ -380,18 +382,21 @@ function App() {
380382
</span>
381383
</div>
382384
{powerUserSummary && (
383-
<Sheet>
384-
<SheetTrigger asChild>
385-
<Button variant="outline" className="flex items-center gap-2">
386-
<span className="text-sm">Power Users:</span>
387-
<span className="font-bold">{powerUserSummary.totalPowerUsers}</span>
388-
</Button>
389-
</SheetTrigger>
390-
<SheetContent className="w-[600px] sm:w-[800px] overflow-y-auto">
391-
<SheetHeader>
392-
<SheetTitle>Power Users Analysis</SheetTitle>
393-
</SheetHeader>
394-
<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">
395400
{/* Power User Summary */}
396401
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
397402
<Card className="p-4">
@@ -507,9 +512,16 @@ function App() {
507512
</Table>
508513
</div>
509514
</Card>
510-
</div>
511-
</SheetContent>
512-
</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>
513525
)}
514526
</div>
515527
</div>
@@ -690,6 +702,7 @@ function App() {
690702
</div>
691703
)}
692704
<Toaster position="top-right" />
705+
<DeploymentFooter />
693706
</div>
694707
);
695708
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getDeploymentInfo } from "@/lib/deployment-info";
2+
3+
export function DeploymentFooter() {
4+
const deployInfo = getDeploymentInfo();
5+
6+
// Don't show footer if no deployment info is available
7+
if (deployInfo.branchName === 'unknown' && deployInfo.deployTime === 'unknown') {
8+
return null;
9+
}
10+
11+
const formatDeployTime = (deployTime: string) => {
12+
if (deployTime === 'unknown') return 'Unknown';
13+
try {
14+
// Try to parse and format the timestamp
15+
const date = new Date(deployTime);
16+
if (isNaN(date.getTime())) return deployTime; // Return as-is if not parseable
17+
return date.toLocaleString();
18+
} catch {
19+
return deployTime; // Return as-is if parsing fails
20+
}
21+
};
22+
23+
return (
24+
<footer className="mt-8 py-4 border-t border-border">
25+
<div className="container max-w-7xl mx-auto px-4">
26+
<div className="flex flex-col sm:flex-row justify-between items-center gap-2 text-xs text-muted-foreground">
27+
<div className="flex items-center gap-4">
28+
{deployInfo.branchName !== 'unknown' && (
29+
<span>
30+
Branch: <span className="font-mono text-foreground">{deployInfo.branchName}</span>
31+
</span>
32+
)}
33+
{deployInfo.deployTime !== 'unknown' && (
34+
<span>
35+
Deployed: <span className="text-foreground">{formatDeployTime(deployInfo.deployTime)}</span>
36+
</span>
37+
)}
38+
</div>
39+
{deployInfo.buildTime !== 'unknown' && (
40+
<span className="text-xs">
41+
Built: {formatDeployTime(deployInfo.buildTime)}
42+
</span>
43+
)}
44+
</div>
45+
</div>
46+
</footer>
47+
);
48+
}

src/lib/deployment-info.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Deployment information that gets injected at build time
2+
export interface DeploymentInfo {
3+
branchName: string;
4+
deployTime: string;
5+
buildTime: string;
6+
}
7+
8+
// Get deployment information from environment variables injected at build time
9+
export function getDeploymentInfo(): DeploymentInfo {
10+
return {
11+
branchName: import.meta.env.VITE_GIT_BRANCH || 'unknown',
12+
deployTime: import.meta.env.VITE_DEPLOY_TIME || 'unknown',
13+
buildTime: import.meta.env.VITE_BUILD_TIME || 'unknown',
14+
};
15+
}

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

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2+
import { render, screen, cleanup } from '@testing-library/react';
3+
import { DeploymentFooter } from '@/components/DeploymentFooter';
4+
5+
// Mock the deployment info module
6+
vi.mock('@/lib/deployment-info', () => ({
7+
getDeploymentInfo: vi.fn(),
8+
}));
9+
10+
import * as deploymentInfoModule from '@/lib/deployment-info';
11+
12+
describe('DeploymentFooter', () => {
13+
const mockGetDeploymentInfo = vi.mocked(deploymentInfoModule.getDeploymentInfo);
14+
15+
beforeEach(() => {
16+
vi.clearAllMocks();
17+
});
18+
19+
afterEach(() => {
20+
vi.restoreAllMocks();
21+
cleanup();
22+
});
23+
24+
it('should display branch name and deploy time when deployment info is available', () => {
25+
const mockDeployInfo = {
26+
branchName: 'main',
27+
deployTime: '2025-06-28T10:00:00Z',
28+
buildTime: '2025-06-28T10:00:00Z',
29+
};
30+
31+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
32+
33+
render(<DeploymentFooter />);
34+
35+
expect(screen.getByText('Branch:')).toBeInTheDocument();
36+
expect(screen.getByText('main')).toBeInTheDocument();
37+
expect(screen.getByText('Deployed:')).toBeInTheDocument();
38+
expect(screen.getByText((content) => content.includes('Built:'))).toBeInTheDocument();
39+
});
40+
41+
it('should display feature branch name correctly', () => {
42+
const mockDeployInfo = {
43+
branchName: 'feature/footer-implementation',
44+
deployTime: '2025-06-28T10:00:00Z',
45+
buildTime: '2025-06-28T10:00:00Z',
46+
};
47+
48+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
49+
50+
render(<DeploymentFooter />);
51+
52+
expect(screen.getByText('feature/footer-implementation')).toBeInTheDocument();
53+
});
54+
55+
it('should not render when deployment info is unknown', () => {
56+
const mockDeployInfo = {
57+
branchName: 'unknown',
58+
deployTime: 'unknown',
59+
buildTime: 'unknown',
60+
};
61+
62+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
63+
64+
const { container } = render(<DeploymentFooter />);
65+
66+
expect(container.firstChild).toBeNull();
67+
});
68+
69+
it('should render when only branch name is available', () => {
70+
const mockDeployInfo = {
71+
branchName: 'main',
72+
deployTime: 'unknown',
73+
buildTime: 'unknown',
74+
};
75+
76+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
77+
78+
render(<DeploymentFooter />);
79+
80+
expect(screen.getByText('Branch:')).toBeInTheDocument();
81+
expect(screen.getByText('main')).toBeInTheDocument();
82+
expect(screen.queryByText('Deployed:')).not.toBeInTheDocument();
83+
expect(screen.queryByText('Built:')).not.toBeInTheDocument();
84+
});
85+
86+
it('should render when only deploy time is available', () => {
87+
const mockDeployInfo = {
88+
branchName: 'unknown',
89+
deployTime: '2025-06-28T10:00:00Z',
90+
buildTime: '2025-06-28T10:00:00Z',
91+
};
92+
93+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
94+
95+
render(<DeploymentFooter />);
96+
97+
expect(screen.queryByText('Branch:')).not.toBeInTheDocument();
98+
expect(screen.getByText('Deployed:')).toBeInTheDocument();
99+
expect(screen.getByText((content) => content.includes('Built:'))).toBeInTheDocument();
100+
});
101+
102+
it('should format date correctly when valid ISO date is provided', () => {
103+
const mockDeployInfo = {
104+
branchName: 'main',
105+
deployTime: '2025-06-28T10:30:45Z',
106+
buildTime: '2025-06-28T10:30:45Z',
107+
};
108+
109+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
110+
111+
render(<DeploymentFooter />);
112+
113+
// Check that the dates are formatted (should have been converted from ISO)
114+
expect(screen.getByText((content) => content.includes('Deployed:'))).toBeInTheDocument();
115+
expect(screen.getByText((content) => content.includes('Built:'))).toBeInTheDocument();
116+
117+
// Check that the formatted dates contain digits (meaning they were processed)
118+
// Using getAllByText since there are multiple elements with the date
119+
const elementsWithDate = screen.getAllByText((content) => content.includes('28') || content.includes('2025'));
120+
expect(elementsWithDate.length).toBeGreaterThan(0);
121+
});
122+
123+
it('should handle invalid date strings gracefully', () => {
124+
const mockDeployInfo = {
125+
branchName: 'main',
126+
deployTime: 'invalid-date',
127+
buildTime: 'another-invalid-date',
128+
};
129+
130+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
131+
132+
render(<DeploymentFooter />);
133+
134+
// Should display the raw string when date parsing fails
135+
expect(screen.getByText('invalid-date')).toBeInTheDocument();
136+
expect(screen.getByText((content) => content.includes('another-invalid-date'))).toBeInTheDocument();
137+
});
138+
139+
it('should have correct CSS classes for styling', () => {
140+
const mockDeployInfo = {
141+
branchName: 'test-styling-branch',
142+
deployTime: '2025-06-28T10:00:00Z',
143+
buildTime: '2025-06-28T10:00:00Z',
144+
};
145+
146+
mockGetDeploymentInfo.mockReturnValue(mockDeployInfo);
147+
148+
const { container } = render(<DeploymentFooter />);
149+
150+
const footer = container.querySelector('footer');
151+
expect(footer).toHaveClass('mt-8', 'py-4', 'border-t', 'border-border');
152+
153+
const branchElement = screen.getByText('test-styling-branch');
154+
expect(branchElement).toHaveClass('font-mono', 'text-foreground');
155+
});
156+
});

0 commit comments

Comments
 (0)