Skip to content

Commit c6fca6d

Browse files
committed
feat: Implement bug fix plan and address critical issues in usage report components
1 parent cc2584d commit c6fca6d

File tree

7 files changed

+88
-51
lines changed

7 files changed

+88
-51
lines changed

src/app/components/usage/actions/charts/chart-line-usage-daily/chart-line-usage-daily.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class ChartLineUsageDailyComponent implements OnChanges {
7676
} else if (this.timeType === 'daily') {
7777
timeKey = line.date.toISOString().split('T')[0];
7878
} else if (this.timeType === 'weekly') {
79-
timeKey = this.getWeekOfYear(line.date).toString();
79+
timeKey = this.getWeekOfYear(line.date);
8080
} else if (this.timeType === 'monthly') {
8181
// get key in format YYYY-MM
8282
timeKey = line.date.toISOString().split('T')[0].slice(0, 7);
@@ -143,7 +143,7 @@ export class ChartLineUsageDailyComponent implements OnChanges {
143143
data = perDay.reduce((acc, curr, index) => {
144144
acc.push([
145145
curr.date.getTime(),
146-
perDay.slice(index > this.rollingDays ? index - this.rollingDays : 0, index).reduce((acc, curr) => acc + curr.total, 0)
146+
perDay.slice(index >= this.rollingDays ? index - this.rollingDays + 1 : 0, index + 1).reduce((acc, curr) => acc + curr.total, 0)
147147
]);
148148
return acc;
149149
}, [] as [number, number][]);
@@ -190,6 +190,6 @@ export class ChartLineUsageDailyComponent implements OnChanges {
190190
d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
191191
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
192192
const weekNo = Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
193-
return weekNo;
193+
return `${d.getUTCFullYear()}-W${weekNo.toString().padStart(2, '0')}`;
194194
}
195195
}

src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
<mat-form-field [class.hide]="dataSource.data.length <= 1">
1414
<mat-label>Filter</mat-label>
15-
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. .github/workflows/build.yml " #input>
15+
<input matInput (keyup)="applyFilter($event)" [placeholder]="tableType === 'workflow' ? 'Ex. build.yml' : tableType === 'repo' ? 'Ex. my-repo' : tableType === 'sku' ? 'Ex. Ubuntu' : 'Ex. username'" #input>
1616
</mat-form-field>
1717
<!-- <div>
1818
<mat-button-toggle-group name="toggleChart" aria-label="Toggle Chart" [value]="tableType"

src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
9797
const column: UsageColumn = {
9898
columnDef: month,
9999
header: month,
100-
cell: (workflowItem: any) => this.currency === 'cost' ? currencyPipe.transform(workflowItem[month]) : decimalPipe.transform(workflowItem[month]),
100+
cell: (workflowItem: any) => this.currency === 'cost' ? currencyPipe.transform(workflowItem[month]) : decimalPipe.transform(Math.round(workflowItem[month]), '1.0-0'),
101101
footer: () => {
102102
const total = this.dataSource.data.reduce((acc, item) => acc + (item as any)[month], 0);
103-
return this.currency === 'cost' ? currencyPipe.transform(total) : decimalPipe.transform(total);
103+
return this.currency === 'cost' ? currencyPipe.transform(total) : decimalPipe.transform(Math.round(total), '1.0-0');
104104
},
105105
date: new Date(line.date),
106106
};
@@ -151,7 +151,7 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
151151
if (!(item as any)[month]) {
152152
(item as any)[month] = 0;
153153
}
154-
const lastMonth: string = new Date(new Date().getFullYear(), this.usageReportService.monthsOrder.indexOf(month) - 1).toLocaleString('default', { month: 'short' });
154+
const lastMonth: string = new Date(column.date!.getFullYear(), column.date!.getMonth() - 1).toLocaleString('default', { month: 'short', year: '2-digit' });
155155
const lastMonthValue = (item as any)[lastMonth];
156156
const percentageChanged = this.calculatePercentageChange(lastMonthValue, (item as any)[month]);
157157
(item as any)[month + 'PercentChange'] = percentageChanged;
@@ -256,24 +256,32 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
256256
columnDef: 'avgTime',
257257
header: 'Avg time',
258258
cell: (workflowItem: WorkflowUsageItem) => `${durationPipe.transform(workflowItem.avgTime)}`,
259-
footer: () => durationPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.avgTime, 0) / this.dataSource.data.length)
259+
footer: () => {
260+
const totalTime = this.dataSource.data.reduce((acc, line) => acc + line.total, 0);
261+
const totalRuns = this.dataSource.data.reduce((acc, line) => acc + line.runs, 0);
262+
return durationPipe.transform(totalRuns > 0 ? totalTime / totalRuns : 0);
263+
}
260264
}, {
261265
columnDef: 'total',
262266
header: 'Total',
263267
cell: (workflowItem: WorkflowUsageItem) => decimalPipe.transform(Math.floor(workflowItem.total)),
264-
footer: () => decimalPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
268+
footer: () => decimalPipe.transform(Math.round(this.dataSource.data.reduce((acc, line) => acc += line.total, 0)), '1.0-0')
265269
});
266270
} else if (this.currency === 'cost') {
267271
columns.push({
268272
columnDef: 'avgCost',
269273
header: 'Avg run',
270274
cell: (workflowItem: WorkflowUsageItem) => currencyPipe.transform(workflowItem.avgCost),
271-
footer: () => currencyPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.cost, 0) / this.dataSource.data.length)
275+
footer: () => {
276+
const totalCost = this.dataSource.data.reduce((acc, line) => acc + line.cost, 0);
277+
const totalRuns = this.dataSource.data.reduce((acc, line) => acc + line.runs, 0);
278+
return currencyPipe.transform(totalRuns > 0 ? totalCost / totalRuns : 0);
279+
}
272280
}, {
273281
columnDef: 'cost',
274282
header: 'Total',
275283
cell: (workflowItem: WorkflowUsageItem) => currencyPipe.transform(workflowItem.cost),
276-
footer: () => currencyPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
284+
footer: () => currencyPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.cost, 0))
277285
});
278286
}
279287
columns[0].footer = () => 'Total';

src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,14 +161,14 @@ export class TableCodespacesUsageComponent implements OnChanges, AfterViewInit {
161161
columnDef: 'total',
162162
header: 'Total seats',
163163
cell: (workflowItem: CodespacesUsageItem) => decimalPipe.transform(Math.floor(workflowItem.total)),
164-
footer: () => decimalPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
164+
footer: () => decimalPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.total, 0))
165165
});
166166
} else if (this.currency === 'cost') {
167167
columns.push({
168168
columnDef: 'cost',
169169
header: 'Total cost',
170170
cell: (workflowItem: CodespacesUsageItem) => currencyPipe.transform(workflowItem.cost),
171-
footer: () => currencyPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
171+
footer: () => currencyPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.cost, 0))
172172
});
173173
}
174174
this.columns = columns;

src/app/components/usage/copilot/table-workflow-usage/table-copilot-usage.component.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ export class TableCopilotUsageComponent implements OnChanges, AfterViewInit {
159159
header: 'Total seats',
160160
cell: (workflowItem: CopilotUsageItem) => decimalPipe.transform(Math.floor(workflowItem.total)),
161161
footer: () => {
162-
if (!this.data) return '';
163-
return decimalPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0));
162+
if (!this.dataSource?.data) return '';
163+
return decimalPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.total, 0));
164164
}
165165
});
166166
} else if (this.currency === 'cost') {
@@ -169,8 +169,8 @@ export class TableCopilotUsageComponent implements OnChanges, AfterViewInit {
169169
header: 'Total cost',
170170
cell: (workflowItem: CopilotUsageItem) => currencyPipe.transform(workflowItem.cost),
171171
footer: () => {
172-
if (!this.data) return '';
173-
return currencyPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0));
172+
if (!this.dataSource?.data) return '';
173+
return currencyPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.cost, 0));
174174
}
175175
});
176176
}

src/app/components/usage/usage.component.ts

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,22 @@ export class UsageComponent implements OnInit, OnDestroy {
5757
this.range.valueChanges.pipe(debounceTime(500)).subscribe(value => {
5858
if (value.start && value.start instanceof Date && !isNaN(value.start.getTime()) &&
5959
value.end && value.end instanceof Date && !isNaN(value.end.getTime())) {
60-
this.usageReportService.applyFilter({
61-
startDate: value.start,
62-
endDate: value.end,
63-
});
60+
// Validate date range: if end is before start, swap them
61+
if (value.end < value.start) {
62+
const temp = value.start;
63+
this.range.controls.start.setValue(value.end, { emitEvent: false });
64+
this.range.controls.end.setValue(temp, { emitEvent: false });
65+
// Update the filter with swapped values
66+
this.usageReportService.applyFilter({
67+
startDate: value.end,
68+
endDate: temp,
69+
});
70+
} else {
71+
this.usageReportService.applyFilter({
72+
startDate: value.start,
73+
endDate: value.end,
74+
});
75+
}
6476
}
6577
})
6678
);
@@ -75,6 +87,7 @@ export class UsageComponent implements OnInit, OnDestroy {
7587
);
7688
this._filteredWorkflows = this.workflowControl.valueChanges.pipe(
7789
startWith(''),
90+
debounceTime(300),
7891
map(value => this._filterWorkflows(value || '')),
7992
);
8093

@@ -112,28 +125,40 @@ export class UsageComponent implements OnInit, OnDestroy {
112125
resolve('');
113126
});
114127
};
115-
const usage = await (type === 'metered' ? this.usageReportService.setUsageReportData(fileText, progressFunction) : type === 'copilot_premium_requests' ? this.usageReportService.setUsageReportCopilotPremiumRequests(fileText, progressFunction) : null);
116-
if (!usage) {
117-
this.status = 'Error parsing file. Please check the file format.';
118-
return;
119-
}
120-
const firstLine = usage.lines[0];
121-
const lastLine = usage.lines[usage.lines.length - 1];
122-
this.minDate = new Date(firstLine && 'date' in firstLine ? firstLine.date : new Date());
123-
this.maxDate = new Date(lastLine && 'date' in lastLine ? lastLine.date : new Date());
124-
// make the date 00:00:00
125-
this.minDate.setHours(0, 0, 0, 0);
126-
this.maxDate.setHours(0, 0, 0, 0);
127-
this.range.controls.start.setValue(this.minDate, { emitEvent: false });
128-
this.range.controls.end.setValue(this.maxDate, { emitEvent: false });
129-
if (type === 'copilot_premium_requests') {
130-
this.usageCopilotPremiumRequests = usage as ModelUsageReport;
131-
} else {
132-
this.usage = usage as UsageReport;
128+
try {
129+
const usage = await (type === 'metered' ? this.usageReportService.setUsageReportData(fileText, progressFunction) : type === 'copilot_premium_requests' ? this.usageReportService.setUsageReportCopilotPremiumRequests(fileText, progressFunction) : null);
130+
if (!usage) {
131+
this.status = 'Error: Unable to parse file. Please ensure it\'s a valid GitHub usage report CSV.';
132+
this.progress = null;
133+
return;
134+
}
135+
if (!usage.lines || usage.lines.length === 0) {
136+
this.status = 'Error: The file contains no usage data. Please check the file format.';
137+
this.progress = null;
138+
return;
139+
}
140+
const firstLine = usage.lines[0];
141+
const lastLine = usage.lines[usage.lines.length - 1];
142+
this.minDate = new Date(firstLine && 'date' in firstLine ? firstLine.date : new Date());
143+
this.maxDate = new Date(lastLine && 'date' in lastLine ? lastLine.date : new Date());
144+
// make the date 00:00:00
145+
this.minDate.setHours(0, 0, 0, 0);
146+
this.maxDate.setHours(0, 0, 0, 0);
147+
this.range.controls.start.setValue(this.minDate, { emitEvent: false });
148+
this.range.controls.end.setValue(this.maxDate, { emitEvent: false });
149+
if (type === 'copilot_premium_requests') {
150+
this.usageCopilotPremiumRequests = usage as ModelUsageReport;
151+
} else {
152+
this.usage = usage as UsageReport;
153+
}
154+
this.status = 'Usage Report';
155+
this.progress = null;
156+
this.cdr.detectChanges();
157+
} catch (error: any) {
158+
this.status = `Error parsing file: ${error.message || 'Invalid format or corrupted data'}. Please ensure you\'re uploading a valid GitHub usage report CSV.`;
159+
this.progress = null;
160+
console.error('Parse error:', error);
133161
}
134-
this.status = 'Usage Report';
135-
this.progress = null;
136-
this.cdr.detectChanges();
137162
}
138163

139164
private _filterWorkflows(workflow: string): string[] {

src/app/usage-report.service.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,12 @@ export class UsageReportService {
133133
}
134134

135135
setValueType(value: 'minutes' | 'cost') {
136-
this.usageReport.lines.forEach(line => {
137-
if (value === 'minutes') {
138-
line.value = (line.quantity) || 0;
139-
} else {
140-
line.value = (line.quantity * line.pricePerUnit) || 0;
141-
}
142-
});
143-
this.usageReportFiltered.next(this.usageReport.lines);
136+
// Don't mutate original data - calculate on-the-fly
137+
const lines = this.usageReport.lines.map(line => ({
138+
...line,
139+
value: value === 'minutes' ? (line.quantity || 0) : (line.quantity * line.pricePerUnit || 0)
140+
}));
141+
this.usageReportFiltered.next(lines);
144142
this.valueType.next(value);
145143
}
146144

@@ -209,7 +207,13 @@ export class UsageReportService {
209207
return line.date >= this.filters.startDate && line.date <= this.filters.endDate;
210208
});
211209
}
212-
this.usageReportFiltered.next(filtered);
210+
// Apply value type transformation
211+
const valueType = this.valueType.value;
212+
const transformedFiltered = filtered.map(line => ({
213+
...line,
214+
value: valueType === 'minutes' ? (line.quantity || 0) : (line.quantity * line.pricePerUnit || 0)
215+
}));
216+
this.usageReportFiltered.next(transformedFiltered);
213217
}
214218

215219
getUsageReportFiltered(): Observable<CustomUsageReportLine[]> {

0 commit comments

Comments
 (0)