Skip to content

Commit 063ab8a

Browse files
Copilotaustenstone
andcommitted
Fix undefined property access error for new CSV format
- Add custom CSV parser with format detection (legacy-15, legacy-14, summarized-12) - Add custom UsageReport and UsageReportLine types supporting optional fields - Track format type and availability of workflow/username data - Update usage.component to import types from service and handle format changes - Update table-workflow-usage to handle missing username/workflow data gracefully - Update chart-pie-user to fall back to repository grouping when username data unavailable - Add UI indicators for summarized report format - Disable workflow/user grouping options when data not available Co-authored-by: austenstone <[email protected]>
1 parent 99fbca2 commit 063ab8a

File tree

7 files changed

+289
-15
lines changed

7 files changed

+289
-15
lines changed

src/app/components/usage/actions/charts/chart-pie-user/chart-pie-user.component.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class ChartPieUserComponent implements OnChanges {
3030
}]
3131
};
3232
updateFromInput: boolean = false;
33+
hasUsernameData: boolean = false;
3334

3435
constructor(
3536
private themeService: ThemingService,
@@ -42,12 +43,19 @@ export class ChartPieUserComponent implements OnChanges {
4243
}
4344

4445
ngOnChanges() {
46+
this.hasUsernameData = this.usageReportService.hasUsernameData;
47+
48+
// If no username data, show usage by repository instead
49+
const groupByField = this.hasUsernameData ? 'username' : 'repositoryName';
50+
const chartTitle = this.hasUsernameData ? 'username' : 'repository';
51+
4552
this.data = this.data.filter((line) => line.unitType === 'minutes');
4653

4754
const aggregatedData = this.data.reduce((acc, line) => {
48-
const index = acc.findIndex((item) => item[0] === line.username);
55+
const fieldValue = (line as any)[groupByField] || 'Unknown';
56+
const index = acc.findIndex((item) => item[0] === fieldValue);
4957
if (index === -1) {
50-
acc.push([line.username, line.value]);
58+
acc.push([fieldValue, line.value]);
5159
} else {
5260
acc[index][1] += line.value;
5361
}
@@ -72,7 +80,7 @@ export class ChartPieUserComponent implements OnChanges {
7280
data
7381
}];
7482
this.options.title = {
75-
text: `${this.currency === 'minutes' ? 'Usage' : 'Cost'} by username`
83+
text: `${this.currency === 'minutes' ? 'Usage' : 'Cost'} by ${chartTitle}`
7684
};
7785
this.options.tooltip = {
7886
...this.options.tooltip,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<mat-select [(value)]="tableType" (selectionChange)="tableType = $event.value; ngOnChanges()">
66
<mat-option value="sku">Runner</mat-option>
77
<mat-option value="repo">Repo</mat-option>
8-
<mat-option value="workflow">Workflow</mat-option>
9-
<mat-option value="user">User</mat-option>
8+
<mat-option value="workflow" [disabled]="!hasWorkflowData" [matTooltip]="!hasWorkflowData ? 'Workflow data not available in summarized reports' : ''">Workflow</mat-option>
9+
<mat-option value="user" [disabled]="!hasUsernameData" [matTooltip]="!hasUsernameData ? 'User data not available in summarized reports' : ''">User</mat-option>
1010
</mat-select>
1111
</mat-form-field>
1212

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

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,31 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
6161
@Input() currency!: string;
6262
dataSource: MatTableDataSource<WorkflowUsageItem | RepoUsageItem | SkuUsageItem> = new MatTableDataSource<any>(); // Initialize the dataSource property
6363
tableType: 'workflow' | 'repo' | 'sku' | 'user' = 'sku';
64+
65+
// Track available grouping options based on CSV format
66+
hasWorkflowData: boolean = false;
67+
hasUsernameData: boolean = false;
6468

6569
@ViewChild(MatPaginator) paginator!: MatPaginator;
6670
@ViewChild(MatSort) sort!: MatSort;
6771

6872
constructor(
69-
private usageReportService: UsageReportService,
73+
public usageReportService: UsageReportService,
7074
) { }
7175

7276
ngOnChanges() {
77+
// Update available grouping options based on the data
78+
this.hasWorkflowData = this.usageReportService.hasWorkflowData;
79+
this.hasUsernameData = this.usageReportService.hasUsernameData;
80+
81+
// Reset table type if current selection is not available
82+
if (this.tableType === 'workflow' && !this.hasWorkflowData) {
83+
this.tableType = 'sku';
84+
}
85+
if (this.tableType === 'user' && !this.hasUsernameData) {
86+
this.tableType = 'sku';
87+
}
88+
7389
this.initializeColumns();
7490
let usage: WorkflowUsageItem[] | RepoUsageItem[] | SkuUsageItem[] = [];
7591
let usageItems: WorkflowUsageItem[] = (usage as WorkflowUsageItem[]);
@@ -82,7 +98,7 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
8298
} else if (this.tableType === 'sku') {
8399
return a.sku === this.usageReportService.formatSku(line.sku);
84100
} else if (this.tableType === 'user') {
85-
return a.username === line.username;
101+
return a.username === (line.username || 'Unknown');
86102
}
87103
return false
88104
});
@@ -130,7 +146,7 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
130146
} else {
131147
acc.push({
132148
workflow: line.workflowName || line.workflowPath || 'Unknown Workflow',
133-
repo: line.repositoryName,
149+
repo: line.repositoryName || 'Unknown Repository',
134150
total: line.quantity,
135151
cost: line.quantity * line.pricePerUnit,
136152
runs: 1,
@@ -139,7 +155,7 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
139155
avgTime: line.value,
140156
[month]: line.value,
141157
sku: this.usageReportService.formatSku(line.sku),
142-
username: line.username,
158+
username: line.username || 'Unknown',
143159
});
144160
}
145161
return acc;

src/app/components/usage/usage.component.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ <h1>Premium Requests</h1>
3131
<mat-icon>close</mat-icon>
3232
</button>
3333
</mat-form-field>
34-
<mat-form-field style="flex: 1; max-width: 500px;" *ngIf="tabSelected === 'actions'">
34+
<mat-form-field style="flex: 1; max-width: 500px;" *ngIf="tabSelected === 'actions' && hasWorkflowData">
3535
<mat-label>Workflow</mat-label>
3636
<input type="text" placeholder="build.yml" aria-label="Number" matInput [formControl]="workflowControl"
3737
[matAutocomplete]="auto" name="workflow">
@@ -46,6 +46,12 @@ <h1>Premium Requests</h1>
4646
</button>
4747
}
4848
</mat-form-field>
49+
<span *ngIf="tabSelected === 'actions' && !hasWorkflowData && formatType === 'summarized'"
50+
class="format-notice"
51+
matTooltip="The summarized report format doesn't include workflow details. Use the detailed usage report for workflow-level data.">
52+
<mat-icon>info</mat-icon>
53+
Summarized report
54+
</span>
4955
<div>
5056
<mat-button-toggle-group name="toggleCurrency" aria-label="Toggle Chart" [value]="currency"
5157
(change)="currency = $event.value; changeCurrency($event.value)">

src/app/components/usage/usage.component.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,19 @@ form {
2121

2222
.tab-icon {
2323
margin-right: 8px;
24+
}
25+
26+
.format-notice {
27+
display: flex;
28+
align-items: center;
29+
gap: 4px;
30+
font-size: 12px;
31+
opacity: 0.7;
32+
cursor: help;
33+
34+
mat-icon {
35+
font-size: 16px;
36+
width: 16px;
37+
height: 16px;
38+
}
2439
}

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { OnInit, ChangeDetectorRef, Component, OnDestroy, isDevMode } from '@angular/core';
22
import { FormControl, FormGroup } from '@angular/forms';
3-
import { UsageReport } from 'github-usage-report/src/types';
43
import { Observable, Subscription, debounceTime, map, startWith } from 'rxjs';
5-
import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
4+
import { CustomUsageReportLine, UsageReport, UsageReportService } from 'src/app/usage-report.service';
65
import { DialogBillingNavigateComponent } from './dialog-billing-navigate';
76
import { MatDialog } from '@angular/material/dialog';
87
import { ModelUsageReport } from 'github-usage-report';
@@ -38,6 +37,8 @@ export class UsageComponent implements OnInit, OnDestroy {
3837
subscriptions: Subscription[] = [];
3938
currency: 'minutes' | 'cost' = 'cost';
4039
tabSelected: 'shared-storage' | 'copilot' | 'actions' = 'actions';
40+
hasWorkflowData: boolean = false;
41+
formatType: 'legacy' | 'summarized' | null = null;
4142

4243
constructor(
4344
private usageReportService: UsageReportService,
@@ -107,6 +108,10 @@ export class UsageComponent implements OnInit, OnDestroy {
107108
this.usageReportService.getWorkflowsFiltered().subscribe((workflows) => {
108109
this.workflows = workflows;
109110
}),
111+
this.usageReportService.formatType.subscribe((formatType) => {
112+
this.formatType = formatType;
113+
this.hasWorkflowData = this.usageReportService.hasWorkflowData;
114+
}),
110115
);
111116
}
112117

0 commit comments

Comments
 (0)