Skip to content

Commit 2199e62

Browse files
authored
feat: Add storage unit selection for actions and packages, allowing toggling between GB-Hours and GB-Months (#2)
1 parent 682958d commit 2199e62

File tree

4 files changed

+118
-7
lines changed

4 files changed

+118
-7
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The GitHub Reports Visualizer transforms complex GitHub billing CSV data into in
2525
- **Cost Center**: Analyze costs by business units
2626
- **Repository**: Smart repository filtering - shows all repos by default, automatically filters by selected organization
2727
- **Cost vs Quantity**: Toggle between cost analysis ($) and usage volume for Actions, Storage, and Packages
28+
- **Storage Units**: Switch between GB-Hours and GB-Months for storage-related services (Actions Storage, Packages)
2829

2930
### 📈 Interactive Visualizations
3031

@@ -133,8 +134,9 @@ The parser automatically detects column names in various formats, making it comp
133134
- Choose repository to see repo-specific analysis
134135
- Filter by cost center for business unit insights
135136
5. **Toggle Breakdown**: Switch between Cost ($) and Usage Volume views for Actions, Storage, and Packages
136-
6. **Analyze Trends**: View complete historical data with no date limitations
137-
7. **Export Insights**: All charts and data remain in your browser for analysis
137+
6. **Choose Storage Units**: For storage services (Actions Storage, Packages), toggle between GB-Hours and GB-Months (1 month ≈ 730 hours)
138+
7. **Analyze Trends**: View complete historical data with no date limitations
139+
8. **Export Insights**: All charts and data remain in your browser for analysis
138140

139141
## 🏗️ Project Structure
140142

src/app/page.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export default function Home() {
4040
copilot: "quantity",
4141
codespaces: "quantity",
4242
});
43+
const [storageUnit, setStorageUnit] = useState<
44+
Record<string, "gb-hours" | "gb-months">
45+
>({
46+
actionsStorage: "gb-hours",
47+
packages: "gb-hours",
48+
});
4349

4450
const handleDataLoaded = (report: GitHubBillingReport) => {
4551
setBillingData(report.data);
@@ -74,6 +80,16 @@ export default function Home() {
7480
[]
7581
);
7682

83+
const handleStorageUnitChange = useCallback(
84+
(serviceType: string, newUnit: "gb-hours" | "gb-months") => {
85+
setStorageUnit((prev) => ({
86+
...prev,
87+
[serviceType]: newUnit,
88+
}));
89+
},
90+
[]
91+
);
92+
7793
// Memoized breakdown change handlers to prevent re-renders
7894
const handleActionsMinutesBreakdownChange = useCallback(
7995
(newBreakdown: "cost" | "quantity") => {
@@ -96,6 +112,20 @@ export default function Home() {
96112
[handleBreakdownChange]
97113
);
98114

115+
const handleActionsStorageUnitChange = useCallback(
116+
(newUnit: "gb-hours" | "gb-months") => {
117+
handleStorageUnitChange("actionsStorage", newUnit);
118+
},
119+
[handleStorageUnitChange]
120+
);
121+
122+
const handlePackagesUnitChange = useCallback(
123+
(newUnit: "gb-hours" | "gb-months") => {
124+
handleStorageUnitChange("packages", newUnit);
125+
},
126+
[handleStorageUnitChange]
127+
);
128+
99129
// Create tabs based on available data
100130
const createTabs = () => {
101131
if (!categorizedData || !filteredData) {
@@ -164,13 +194,15 @@ export default function Home() {
164194
handleFiltersChange("actionsStorage", filtered)
165195
}
166196
onBreakdownChange={handleActionsStorageBreakdownChange}
197+
onStorageUnitChange={handleActionsStorageUnitChange}
167198
serviceType="actionsStorage"
168199
/>
169200
<ServiceChart
170201
data={filteredData.actionsStorage}
171202
title="GitHub Actions Storage"
172203
serviceType="actionsStorage"
173204
breakdown={breakdown.actionsStorage}
205+
storageUnit={storageUnit.actionsStorage}
174206
/>
175207
</div>
176208
),
@@ -186,13 +218,15 @@ export default function Home() {
186218
handleFiltersChange("packages", filtered)
187219
}
188220
onBreakdownChange={handlePackagesBreakdownChange}
221+
onStorageUnitChange={handlePackagesUnitChange}
189222
serviceType="packages"
190223
/>
191224
<ServiceChart
192225
data={filteredData.packages}
193226
title="GitHub Packages"
194227
serviceType="packages"
195228
breakdown={breakdown.packages}
229+
storageUnit={storageUnit.packages}
196230
/>
197231
</div>
198232
),

src/components/charts/ServiceChart.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface ServiceChartProps {
2828
useSkuAnalysis?: boolean; // Override to use SKU-based analysis instead of repository-based
2929
breakdown?: "cost" | "quantity"; // Whether to breakdown by cost or quantity
3030
hasMultipleOrganizations?: boolean; // Whether to show organization breakdown charts
31+
storageUnit?: "gb-hours" | "gb-months"; // Unit for displaying storage data
3132
}
3233

3334
const COLORS = [
@@ -44,12 +45,21 @@ const COLORS = [
4445
"#64748b",
4546
];
4647

48+
// Conversion constant: 1 month ≈ 730 hours (average)
49+
const HOURS_PER_MONTH = 730;
50+
51+
// Convert GB-hours to GB-months
52+
function convertToGBMonths(gbHours: number): number {
53+
return gbHours / HOURS_PER_MONTH;
54+
}
55+
4756
export function ServiceChart({
4857
data,
4958
title,
5059
serviceType,
5160
useSkuAnalysis = false,
5261
breakdown = "quantity",
62+
storageUnit = "gb-hours",
5363
}: ServiceChartProps) {
5464
if (!data || data.length === 0) {
5565
return (
@@ -84,6 +94,7 @@ export function ServiceChart({
8494
title={title}
8595
serviceType={serviceType}
8696
breakdown={breakdown}
97+
storageUnit={storageUnit}
8798
/>
8899
);
89100
}
@@ -117,6 +128,7 @@ export function ServiceChart({
117128
serviceType={serviceType}
118129
breakdown={breakdown}
119130
hasMultipleOrganizations={hasMultipleOrganizations}
131+
storageUnit={storageUnit}
120132
/>
121133
);
122134
} else {
@@ -126,6 +138,7 @@ export function ServiceChart({
126138
title={title}
127139
serviceType={serviceType}
128140
breakdown={breakdown}
141+
storageUnit={storageUnit}
129142
/>
130143
);
131144
}
@@ -136,6 +149,7 @@ function RepositorySpecificChart({
136149
title,
137150
serviceType,
138151
breakdown = "quantity",
152+
storageUnit = "gb-hours",
139153
}: ServiceChartProps) {
140154
const repository = data[0]?.repository || "Unknown Repository";
141155

@@ -187,6 +201,10 @@ function RepositorySpecificChart({
187201
if (serviceType === "actionsMinutes") {
188202
return `${value.toLocaleString()} min`;
189203
} else if (serviceType === "actionsStorage" || serviceType === "packages") {
204+
if (storageUnit === "gb-months") {
205+
const gbMonths = convertToGBMonths(value);
206+
return `${gbMonths.toLocaleString(undefined, { maximumFractionDigits: 2 })} GB·mo`;
207+
}
190208
return `${value.toLocaleString()} GB·h`;
191209
} else if (serviceType === "copilot") {
192210
return `${value.toFixed(2)} users`;
@@ -682,6 +700,7 @@ function RepositoryBasedChart({
682700
serviceType,
683701
breakdown = "quantity",
684702
hasMultipleOrganizations = false,
703+
storageUnit = "gb-hours",
685704
}: ServiceChartProps) {
686705
// Get top 10 repositories by the selected breakdown metric
687706
const repoTotals = data.reduce((acc, item) => {
@@ -832,6 +851,10 @@ function RepositoryBasedChart({
832851
if (serviceType === "actionsMinutes") {
833852
return `${value.toLocaleString()} min`;
834853
} else if (serviceType === "actionsStorage" || serviceType === "packages") {
854+
if (storageUnit === "gb-months") {
855+
const gbMonths = convertToGBMonths(value);
856+
return `${gbMonths.toLocaleString(undefined, { maximumFractionDigits: 2 })} GB·mo`;
857+
}
835858
return `${value.toLocaleString()} GB·h`;
836859
} else if (serviceType === "copilot") {
837860
return `${value.toFixed(2)} users`;
@@ -1084,6 +1107,7 @@ function SKUBasedChart({
10841107
title,
10851108
serviceType,
10861109
breakdown = "quantity",
1110+
storageUnit = "gb-hours",
10871111
}: ServiceChartProps) {
10881112
// Check if we have multiple organizations to show organization breakdown
10891113
const organizations = Array.from(
@@ -1163,6 +1187,10 @@ function SKUBasedChart({
11631187
if (serviceType === "actionsMinutes") {
11641188
return `${value.toLocaleString()} min`;
11651189
} else if (serviceType === "actionsStorage" || serviceType === "packages") {
1190+
if (storageUnit === "gb-months") {
1191+
const gbMonths = convertToGBMonths(value);
1192+
return `${gbMonths.toLocaleString(undefined, { maximumFractionDigits: 2 })} GB·mo`;
1193+
}
11661194
return `${value.toLocaleString()} GB·h`;
11671195
} else if (serviceType === "copilot") {
11681196
return `${value.toFixed(2)} users`;

src/components/ui/DataFilters.tsx

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ interface DataFiltersProps {
55
data: ServiceData[];
66
onFiltersChange: (filteredData: ServiceData[]) => void;
77
onBreakdownChange?: (breakdown: "cost" | "quantity") => void;
8+
onStorageUnitChange?: (unit: "gb-hours" | "gb-months") => void;
89
serviceType?:
910
| "actionsMinutes"
1011
| "actionsStorage"
@@ -22,12 +23,14 @@ interface FilterState {
2223
costCenter: string;
2324
repository: string;
2425
breakdown: "cost" | "quantity";
26+
storageUnit: "gb-hours" | "gb-months";
2527
}
2628

2729
export function DataFilters({
2830
data,
2931
onFiltersChange,
3032
onBreakdownChange,
33+
onStorageUnitChange,
3134
serviceType,
3235
}: DataFiltersProps) {
3336
const [filters, setFilters] = useState<FilterState>({
@@ -36,6 +39,7 @@ export function DataFilters({
3639
costCenter: "",
3740
repository: "",
3841
breakdown: "quantity",
42+
storageUnit: "gb-hours",
3943
});
4044

4145
// Get unique values for dropdowns
@@ -129,6 +133,20 @@ export function DataFilters({
129133
}
130134
}, []); // Only run on mount
131135

136+
// Notify parent of storage unit changes
137+
useEffect(() => {
138+
if (onStorageUnitChange) {
139+
onStorageUnitChange(filters.storageUnit);
140+
}
141+
}, [filters.storageUnit]);
142+
143+
// Notify parent of initial storage unit on mount
144+
useEffect(() => {
145+
if (onStorageUnitChange) {
146+
onStorageUnitChange(filters.storageUnit);
147+
}
148+
}, []); // Only run on mount
149+
132150
const handleFilterChange = (
133151
key: string,
134152
value: string | { start: string; end: string }
@@ -156,6 +174,7 @@ export function DataFilters({
156174
costCenter: "",
157175
repository: "",
158176
breakdown: "quantity",
177+
storageUnit: "gb-hours",
159178
});
160179
};
161180

@@ -172,6 +191,18 @@ export function DataFilters({
172191
serviceType === "actionsStorage" ||
173192
serviceType === "packages";
174193

194+
// Show storage unit selector for storage-related services
195+
const showStorageUnitSelector =
196+
serviceType === "actionsStorage" || serviceType === "packages";
197+
198+
// Calculate grid columns based on visible filters
199+
const gridCols =
200+
showBreakdownSelector && showStorageUnitSelector
201+
? "lg:grid-cols-6"
202+
: showBreakdownSelector
203+
? "lg:grid-cols-5"
204+
: "lg:grid-cols-4";
205+
175206
return (
176207
<div className="bg-gray-800/30 rounded-lg p-6 mb-6">
177208
<div className="flex items-center justify-between mb-4">
@@ -186,11 +217,7 @@ export function DataFilters({
186217
)}
187218
</div>
188219

189-
<div
190-
className={`grid grid-cols-1 md:grid-cols-2 ${
191-
showBreakdownSelector ? "lg:grid-cols-5" : "lg:grid-cols-4"
192-
} gap-4`}
193-
>
220+
<div className={`grid grid-cols-1 md:grid-cols-2 ${gridCols} gap-4`}>
194221
{/* Date Range */}
195222
<div className="space-y-2">
196223
<label className="text-sm text-gray-400">Date Range</label>
@@ -298,6 +325,26 @@ export function DataFilters({
298325
</select>
299326
</div>
300327
)}
328+
329+
{/* Storage Unit Selector */}
330+
{showStorageUnitSelector && (
331+
<div className="space-y-2">
332+
<label className="text-sm text-gray-400">Storage Unit</label>
333+
<select
334+
value={filters.storageUnit}
335+
onChange={(e) =>
336+
handleFilterChange(
337+
"storageUnit",
338+
e.target.value as "gb-hours" | "gb-months"
339+
)
340+
}
341+
className="w-full px-3 py-2 bg-gray-700/50 border border-gray-600 rounded-md text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
342+
>
343+
<option value="gb-hours">GB-Hours</option>
344+
<option value="gb-months">GB-Months</option>
345+
</select>
346+
</div>
347+
)}
301348
</div>
302349
</div>
303350
);

0 commit comments

Comments
 (0)