Skip to content

Commit ce0102a

Browse files
authored
Merge branch 'main' into dependabot/npm_and_yarn/npm-dependencies-3c4bcd6eae
2 parents 159469d + 03fbb91 commit ce0102a

File tree

12 files changed

+1010
-58
lines changed

12 files changed

+1010
-58
lines changed

.github/CODEOWNERS

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# CODEOWNERS file for github-copilot-premium-reqs-usage
2+
#
3+
# This file defines code ownership for the repository.
4+
# Code owners are automatically requested for review when someone opens a pull request that modifies code that they own.
5+
#
6+
# More info: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
7+
8+
# Global rule - all files are owned by the main contributors
9+
* @rajbos
10+
11+
# Specific rules can be added below for different paths if needed
12+
# Example:
13+
# /src/ @rajbos
14+
# /docs/ @rajbos

.github/workflows/ci.yml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
name: CI Build
2+
3+
on:
4+
# Trigger on all pull requests to validate builds
5+
pull_request:
6+
branches: [ main ]
7+
paths-ignore:
8+
- '**.md'
9+
- 'LICENSE'
10+
- 'SECURITY.md'
11+
12+
# Trigger on pushes to branches (but not main, which is handled by deploy workflow)
13+
push:
14+
branches-ignore: [ main ]
15+
paths-ignore:
16+
- '**.md'
17+
- 'LICENSE'
18+
- 'SECURITY.md'
19+
20+
# Allow only one concurrent CI run per branch/PR
21+
concurrency:
22+
group: "ci-${{ github.head_ref || github.ref }}"
23+
cancel-in-progress: true
24+
25+
permissions:
26+
contents: read
27+
28+
jobs:
29+
# Build validation job
30+
build:
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: Checkout
34+
uses: actions/checkout@v4
35+
36+
- name: Setup Node
37+
uses: actions/setup-node@v4
38+
with:
39+
node-version: "18"
40+
cache: 'npm'
41+
42+
- name: Install dependencies
43+
run: npm install
44+
45+
- name: Lint code
46+
run: npm run lint
47+
48+
- name: Run tests
49+
run: npm run test:run
50+
51+
- name: Build application
52+
run: npm run build
53+
54+
- name: Build for GitHub Pages
55+
run: npm run build:pages
56+
57+
- name: Validate build outputs
58+
run: |
59+
echo "Validating standard build..."
60+
if [ ! -f "dist/index.html" ]; then
61+
echo "❌ Standard build failed: dist/index.html not found"
62+
exit 1
63+
fi
64+
echo "✅ Standard build output validated"
65+
66+
echo "Validating GitHub Pages build..."
67+
if [ ! -f "dist/index.html" ]; then
68+
echo "❌ GitHub Pages build failed: dist/index.html not found"
69+
exit 1
70+
fi
71+
echo "✅ GitHub Pages build output validated"
72+
73+
- name: Report build success
74+
run: |
75+
echo "🎉 CI Build completed successfully!"
76+
echo "✅ Linting passed (with warnings allowed)"
77+
echo "✅ All $( npm run test:run 2>&1 | grep -o '[0-9]\+ passed' | head -1 ) tests passed"
78+
echo "✅ Standard build completed"
79+
echo "✅ GitHub Pages build completed"

eslint.config.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import js from '@eslint/js';
2+
import globals from 'globals';
3+
import reactHooks from 'eslint-plugin-react-hooks';
4+
import reactRefresh from 'eslint-plugin-react-refresh';
5+
import tseslint from 'typescript-eslint';
6+
7+
export default tseslint.config(
8+
{ ignores: ['dist', 'node_modules'] },
9+
{
10+
extends: [js.configs.recommended, ...tseslint.configs.recommended],
11+
files: ['**/*.{ts,tsx}'],
12+
languageOptions: {
13+
ecmaVersion: 2020,
14+
globals: globals.browser,
15+
},
16+
plugins: {
17+
'react-hooks': reactHooks,
18+
'react-refresh': reactRefresh,
19+
},
20+
rules: {
21+
...reactHooks.configs.recommended.rules,
22+
'react-refresh/only-export-components': [
23+
'warn',
24+
{ allowConstantExport: true },
25+
],
26+
// Make ESLint more lenient for existing codebase
27+
'@typescript-eslint/no-unused-vars': 'warn',
28+
'@typescript-eslint/no-explicit-any': 'warn',
29+
},
30+
},
31+
)

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 108 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
PowerUserDailyBreakdown,
2525
ExceededRequestDetail,
2626
ProjectedUserData,
27+
MonthOption,
2728
aggregateDataByDay,
2829
parseCSV,
2930
getModelUsageSummary,
@@ -40,12 +41,18 @@ import {
4041
getTotalRequestsForUsersExceedingQuota,
4142
getProjectedUsersExceedingQuota,
4243
getProjectedUsersExceedingQuotaDetails,
44+
getAvailableMonths,
45+
filterDataByMonth,
4346
EXCESS_REQUEST_COST
4447
} from "@/lib/utils";
48+
import { MonthSelector } from "@/components/MonthSelector";
4549

4650
function App() {
4751
const [showPrivacyBanner, setShowPrivacyBanner] = useState(true);
4852
const [data, setData] = useState<CopilotUsageData[] | null>(null);
53+
const [rawData, setRawData] = useState<CopilotUsageData[] | null>(null); // Store original unfiltered data
54+
const [availableMonths, setAvailableMonths] = useState<MonthOption[]>([]);
55+
const [selectedMonth, setSelectedMonth] = useState<string>('');
4956
const [aggregatedData, setAggregatedData] = useState<AggregatedData[]>([]);
5057
const [uniqueModels, setUniqueModels] = useState<string[]>([]);
5158
const [modelSummary, setModelSummary] = useState<ModelUsageSummary[]>([]);
@@ -81,6 +88,67 @@ function App() {
8188
setProjectedUsersData(projectedDetails);
8289
}
8390
}, [selectedPlan, data]);
91+
92+
// Reprocess data when month selection changes
93+
useEffect(() => {
94+
if (rawData && selectedMonth) {
95+
processDataForMonth(rawData, selectedMonth);
96+
}
97+
}, [selectedMonth, rawData, selectedPlan]);
98+
99+
/**
100+
* Process data for a specific month and update all derived state
101+
* This function aggregates and processes data for the selected month only
102+
*/
103+
const processDataForMonth = useCallback((rawData: CopilotUsageData[], month: string) => {
104+
// Filter data by selected month
105+
const filteredData = filterDataByMonth(rawData, month);
106+
setData(filteredData);
107+
108+
// Get unique models from filtered data
109+
const models = Array.from(new Set(filteredData.map(item => item.model)));
110+
setUniqueModels(models);
111+
112+
// Aggregate data by day and model for the selected month
113+
const aggregated = aggregateDataByDay(filteredData);
114+
setAggregatedData(aggregated);
115+
116+
// Get model usage summary for the selected month
117+
const summary = getModelUsageSummary(filteredData);
118+
setModelSummary(summary);
119+
120+
// Get daily model data for bar chart for the selected month
121+
const dailyData = getDailyModelData(filteredData);
122+
setDailyModelData(dailyData);
123+
124+
// Get power users data for the selected month
125+
const powerUsers = getPowerUsers(filteredData);
126+
setPowerUserSummary(powerUsers);
127+
128+
// Get power user daily breakdown for the stacked bar chart for the selected month
129+
const powerUserNames = powerUsers.powerUsers.map(user => user.user);
130+
const powerUserBreakdown = getPowerUserDailyBreakdown(filteredData, powerUserNames);
131+
setPowerUserDailyBreakdown(powerUserBreakdown);
132+
133+
// Get count of users exceeding quota for top bar display for the selected month
134+
const exceedingUsersCount = getUniqueUsersExceedingQuota(filteredData, selectedPlan);
135+
setUsersExceedingQuota(exceedingUsersCount);
136+
137+
// Get projected count of users who will exceed quota by month-end for the selected month
138+
const projectedExceedingUsersCount = getProjectedUsersExceedingQuota(filteredData, selectedPlan);
139+
setProjectedUsersExceedingQuota(projectedExceedingUsersCount);
140+
141+
// Get projected users details for the selected month
142+
const projectedDetails = getProjectedUsersExceedingQuotaDetails(filteredData, selectedPlan);
143+
setProjectedUsersData(projectedDetails);
144+
145+
// Get the last date available in the filtered CSV for the selected month
146+
const lastDate = getLastDateFromData(filteredData);
147+
setLastDateAvailable(lastDate);
148+
149+
// Reset selected power user when month changes
150+
setSelectedPowerUser(null);
151+
}, [selectedPlan]);
84152

85153
const handlePowerUserSelect = useCallback((userName: string | null) => {
86154
setSelectedPowerUser(userName);
@@ -136,54 +204,25 @@ function App() {
136204
}
137205

138206
const parsedData = parseCSV(csvContent);
139-
setData(parsedData);
140-
141-
// Get unique models
142-
const models = Array.from(new Set(parsedData.map(item => item.model)));
143-
setUniqueModels(models);
144-
145-
// Aggregate data by day and model
146-
const aggregated = aggregateDataByDay(parsedData);
147-
setAggregatedData(aggregated);
148-
149-
// Get model usage summary
150-
const summary = getModelUsageSummary(parsedData);
151-
setModelSummary(summary);
152-
153-
// Get daily model data for bar chart
154-
const dailyData = getDailyModelData(parsedData);
155-
setDailyModelData(dailyData);
156-
157-
// Get power users data
158-
const powerUsers = getPowerUsers(parsedData);
159-
setPowerUserSummary(powerUsers);
160-
161-
// Get power user daily breakdown for the stacked bar chart
162-
const powerUserNames = powerUsers.powerUsers.map(user => user.user);
163-
const powerUserBreakdown = getPowerUserDailyBreakdown(parsedData, powerUserNames);
164-
setPowerUserDailyBreakdown(powerUserBreakdown);
165207

166-
// Get count of users exceeding quota for top bar display
167-
const exceedingUsersCount = getUniqueUsersExceedingQuota(parsedData, selectedPlan);
168-
setUsersExceedingQuota(exceedingUsersCount);
208+
// Store raw unfiltered data
209+
setRawData(parsedData);
169210

170-
// Get projected count of users who will exceed quota by month-end
171-
const projectedExceedingUsersCount = getProjectedUsersExceedingQuota(parsedData, selectedPlan);
172-
setProjectedUsersExceedingQuota(projectedExceedingUsersCount);
211+
// Extract available months (current and previous month)
212+
const months = getAvailableMonths(parsedData);
213+
setAvailableMonths(months);
173214

174-
// Get projected users details
175-
const projectedDetails = getProjectedUsersExceedingQuotaDetails(parsedData, selectedPlan);
176-
setProjectedUsersData(projectedDetails);
215+
// Auto-select current month if available, otherwise select the first (most recent) month
216+
const defaultMonth = months.find(m => m.isCurrentMonth)?.value || months[0]?.value || '';
217+
setSelectedMonth(defaultMonth);
177218

178-
// Get the last date available in the CSV
179-
const lastDate = getLastDateFromData(parsedData);
180-
setLastDateAvailable(lastDate);
181-
182-
// Reset selected power user when new data is loaded
183-
setSelectedPowerUser(null);
219+
// Process data for the default selected month
220+
if (defaultMonth) {
221+
processDataForMonth(parsedData, defaultMonth);
222+
}
184223

185224
setIsProcessing(false);
186-
toast.success(`Loaded ${parsedData.length} records successfully`);
225+
toast.success(`Loaded ${parsedData.length.toLocaleString()} records successfully`);
187226
} catch (error) {
188227
// Provide user-friendly error messages
189228
let errorMessage = "Failed to parse CSV file. Please check the format.";
@@ -217,6 +256,9 @@ function App() {
217256
setIsProcessing(false);
218257
toast.error(errorMessage);
219258
setData(null);
259+
setRawData(null);
260+
setAvailableMonths([]);
261+
setSelectedMonth('');
220262
setAggregatedData([]);
221263
setModelSummary([]);
222264
setDailyModelData([]);
@@ -229,7 +271,7 @@ function App() {
229271
};
230272

231273
reader.readAsText(file);
232-
}, []);
274+
}, [processDataForMonth]);
233275

234276
const handleFileUpload = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
235277
if (isProcessing) return;
@@ -557,7 +599,20 @@ function App() {
557599
{data && data.length > 0 && (
558600
<div className="space-y-8">
559601
<div>
560-
<h2 className="text-2xl font-semibold mb-2">Usage Statistics</h2>
602+
<div className="flex items-center justify-between mb-4">
603+
<div>
604+
<h2 className="text-2xl font-semibold mb-2">Usage Statistics</h2>
605+
</div>
606+
<div className="flex items-center gap-4">
607+
<MonthSelector
608+
availableMonths={availableMonths}
609+
selectedMonth={selectedMonth}
610+
onMonthChange={setSelectedMonth}
611+
disabled={isProcessing}
612+
data={rawData}
613+
/>
614+
</div>
615+
</div>
561616
<Separator className="mb-4" />
562617
<div className="mb-4">
563618
<Card>
@@ -588,37 +643,37 @@ function App() {
588643
</span>
589644
</div>
590645
<div
591-
className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all duration-200 rounded-md px-2 py-1 group border border-orange-200 hover:border-orange-300 hover:shadow-sm bg-orange-25 dark:bg-orange-950/10"
646+
className="xebia-action-button"
592647
onClick={() => setShowProjectedUsersDialog(true)}
593648
title="Click to see the users projected to exceed quota"
594649
>
595650
<span className="text-sm text-muted-foreground">Projected to Exceed by Month-End:</span>
596-
<span className="text-lg font-bold text-orange-600 group-hover:text-orange-700 transition-colors">
651+
<span className="value">
597652
{projectedUsersExceedingQuota.toLocaleString()}
598653
</span>
599-
<ChevronRight className="h-3 w-3 text-orange-600 opacity-60 group-hover:opacity-100 group-hover:translate-x-0.5 transition-all duration-200" />
654+
<ChevronRight className="icon" />
600655
</div>
601656
<div
602-
className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-orange-50 dark:hover:bg-orange-900/20 transition-all duration-200 rounded-md px-2 py-1 group border border-orange-200 hover:border-orange-300 hover:shadow-sm bg-orange-25 dark:bg-orange-950/10"
657+
className="xebia-action-button"
603658
onClick={() => setShowPotentialCostDetails(true)}
604659
title="Click to see cost breakdown"
605660
>
606661
<span className="text-sm text-muted-foreground">Potential Cost:</span>
607-
<span className="text-lg font-bold text-orange-600 group-hover:text-orange-700 transition-colors">
662+
<span className="value">
608663
${(data.reduce((sum, item) => sum + item.requestsUsed, 0) * EXCESS_REQUEST_COST).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
609664
</span>
610-
<ChevronRight className="h-3 w-3 text-orange-600 opacity-60 group-hover:opacity-100 group-hover:translate-x-0.5 transition-all duration-200" />
665+
<ChevronRight className="icon" />
611666
</div>
612667
{powerUserSummary && (
613668
<Sheet>
614669
<SheetTrigger asChild>
615670
<div
616-
className="inline-flex items-center gap-1.5 cursor-pointer hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all duration-200 rounded-md px-2 py-1 group border border-blue-200 hover:border-blue-300 hover:shadow-sm bg-blue-25 dark:bg-blue-950/10"
671+
className="xebia-action-button"
617672
title="Click to view power users analysis"
618673
>
619674
<span className="text-sm text-muted-foreground">Power Users:</span>
620-
<span className="text-lg font-bold text-blue-600 group-hover:text-blue-700 transition-colors">{powerUserSummary.totalPowerUsers}</span>
621-
<ChevronRight className="h-3 w-3 text-blue-600 opacity-60 group-hover:opacity-100 group-hover:translate-x-0.5 transition-all duration-200" />
675+
<span className="value">{powerUserSummary.totalPowerUsers}</span>
676+
<ChevronRight className="icon" />
622677
</div>
623678
</SheetTrigger>
624679
<SheetContent side="bottom" className="h-[90vh] max-w-[90%] mx-auto overflow-y-auto">

0 commit comments

Comments
 (0)