Skip to content

Commit a2d8812

Browse files
committed
Add Codespaces and TableCodespacesUsageComponent
1 parent 98cc1b6 commit a2d8812

11 files changed

+316
-3
lines changed

src/app/app.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { LineUsageTimeComponent } from './components/usage/shared-storage/charts
2222
import { CopilotComponent } from './components/usage/copilot/copilot.component';
2323
import { DialogBillingNavigateComponent } from './components/usage/dialog-billing-navigate';
2424
import { TableCopilotUsageComponent } from './components/usage/copilot/table-workflow-usage/table-copilot-usage.component';
25+
import { CodespacesComponent } from './components/usage/codespaces/codespaces.component';
26+
import { TableCodespacesUsageComponent } from './components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component';
2527

2628
@NgModule({
2729
declarations: [
@@ -39,7 +41,9 @@ import { TableCopilotUsageComponent } from './components/usage/copilot/table-wor
3941
LineUsageTimeComponent,
4042
DialogBillingNavigateComponent,
4143
CopilotComponent,
42-
TableCopilotUsageComponent
44+
TableCopilotUsageComponent,
45+
CodespacesComponent,
46+
TableCodespacesUsageComponent
4347
],
4448
imports: [
4549
BrowserModule,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<app-table-codespaces-usage [data]="data" [currency]="currency"></app-table-codespaces-usage>

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

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { CodespacesComponent } from './codespaces.component';
4+
5+
describe('CodespacesComponent', () => {
6+
let component: CodespacesComponent;
7+
let fixture: ComponentFixture<CodespacesComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [CodespacesComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(CodespacesComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Component, Input } from '@angular/core';
2+
import { CustomUsageReportLine } from 'src/app/usage-report.service';
3+
4+
@Component({
5+
selector: 'app-codespaces',
6+
templateUrl: './codespaces.component.html',
7+
styleUrl: './codespaces.component.scss'
8+
})
9+
export class CodespacesComponent {
10+
@Input() data!: CustomUsageReportLine[];
11+
@Input() currency!: string;
12+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<div class="table-responsive">
2+
<div class="table-control">
3+
<mat-form-field>
4+
<mat-label>Grouping</mat-label>
5+
<mat-select [(value)]="tableType" (selectionChange)="tableType = $event.value; ngOnChanges()">
6+
<mat-option value="sku">Product</mat-option>
7+
<mat-option value="repo">Repository</mat-option>
8+
<mat-option value="user">User</mat-option>
9+
</mat-select>
10+
</mat-form-field>
11+
<mat-form-field [class.hide]="dataSource.data.length <= 1">
12+
<mat-label>Filter</mat-label>
13+
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. .github/workflows/build.yml " #input>
14+
</mat-form-field>
15+
</div>
16+
17+
<div class="table-container">
18+
<table mat-table [dataSource]="dataSource" matSort matSortActive="total" matSortDirection="desc">
19+
@for (column of columns; track column) {
20+
<ng-container [matColumnDef]="column.columnDef" [sticky]="column.sticky">
21+
<th mat-header-cell *matHeaderCellDef mat-sort-header>
22+
{{column.header}}
23+
</th>
24+
<td mat-cell *matCellDef="let row">
25+
{{column.cell(row)}}
26+
</td>
27+
<td mat-footer-cell *matFooterCellDef> <b>{{ column.footer ? column.footer() : '' }}</b> </td>
28+
</ng-container>
29+
}
30+
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
31+
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
32+
<tr mat-footer-row *matFooterRowDef="displayedColumns"></tr>
33+
34+
<tr class="mat-row" *matNoDataRow>
35+
<td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td>
36+
</tr>
37+
</table>
38+
</div>
39+
40+
<mat-paginator [pageSizeOptions]="[10, 25, 100]" aria-label="Select page of users"></mat-paginator>
41+
</div>

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

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { TableCodespacesUsageComponent } from './table-codespaces-usage.component';
4+
5+
describe('TableCodespacesUsageComponent', () => {
6+
let component: TableCodespacesUsageComponent;
7+
let fixture: ComponentFixture<TableCodespacesUsageComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [TableCodespacesUsageComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(TableCodespacesUsageComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { AfterViewInit, Component, Input, OnChanges, ViewChild } from '@angular/core';
2+
import { MatPaginator } from '@angular/material/paginator';
3+
import { MatTableDataSource } from '@angular/material/table';
4+
import { MatSort } from '@angular/material/sort';
5+
import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
6+
7+
interface UsageColumn {
8+
columnDef: string;
9+
header: string;
10+
cell: (element: any) => any;
11+
footer?: () => any;
12+
sticky?: boolean;
13+
}
14+
15+
interface CodespacesUsageItem {
16+
runs: number;
17+
total: number;
18+
cost: number;
19+
pricePerUnit: number;
20+
owner: string;
21+
username: string;
22+
sku: string;
23+
unitType: string;
24+
repositorySlug: string;
25+
sticky?: boolean;
26+
}
27+
28+
@Component({
29+
selector: 'app-table-codespaces-usage',
30+
templateUrl: './table-codespaces-usage.component.html',
31+
styleUrl: './table-codespaces-usage.component.scss'
32+
})
33+
export class TableCodespacesUsageComponent implements OnChanges, AfterViewInit {
34+
columns = [] as UsageColumn[];
35+
displayedColumns = this.columns.map(c => c.columnDef);
36+
@Input() data!: CustomUsageReportLine[];
37+
@Input() currency!: string;
38+
dataSource: MatTableDataSource<CodespacesUsageItem> = new MatTableDataSource<any>(); // Initialize the dataSource property
39+
tableType: 'sku' | 'repo' | 'user' = 'sku';
40+
41+
@ViewChild(MatPaginator) paginator!: MatPaginator;
42+
@ViewChild(MatSort) sort!: MatSort;
43+
44+
constructor(
45+
private usageReportService: UsageReportService,
46+
) { }
47+
48+
ngOnChanges() {
49+
this.initializeColumns();
50+
let usage: CodespacesUsageItem[] = [];
51+
let usageItems: CodespacesUsageItem[] = (usage as CodespacesUsageItem[]);
52+
usageItems = this.data.reduce((acc, line) => {
53+
const item = acc.find(a => {
54+
if (this.tableType === 'sku') {
55+
return a.sku === line.sku;
56+
} else if (this.tableType === 'repo') {
57+
return a.repositorySlug === line.repositorySlug;
58+
} else if (this.tableType === 'user') {
59+
return a.username === line.username;
60+
}
61+
return false;
62+
});
63+
const month: string = line.date.toLocaleString('default', { month: 'short' });
64+
if (item) {
65+
if ((item as any)[month]) {
66+
(item as any)[month] += line.value;
67+
} else {
68+
(item as any)[month] = line.value || 0;
69+
}
70+
item.total += line.value;
71+
if (!this.columns.find(c => c.columnDef === month)) {
72+
this.columns.push({
73+
columnDef: month,
74+
header: month,
75+
cell: (workflowItem: any) => this.currency === 'cost' ? currencyPipe.transform(workflowItem[month]) : decimalPipe.transform(workflowItem[month]),
76+
footer: () => {
77+
const total = this.dataSource.data.reduce((acc, item) => acc + (item as any)[month], 0);
78+
return this.currency === 'cost' ? currencyPipe.transform(total) : decimalPipe.transform(total);
79+
}
80+
});
81+
}
82+
item.cost += line.quantity * line.pricePerUnit;
83+
item.total += line.quantity;
84+
item.runs++;
85+
} else {
86+
acc.push({
87+
owner: line.owner,
88+
total: line.quantity,
89+
cost: line.quantity * line.pricePerUnit,
90+
runs: 1,
91+
pricePerUnit: line.pricePerUnit || 0,
92+
[month]: line.value,
93+
sku: line.sku,
94+
unitType: line.unitType,
95+
repositorySlug: line.repositorySlug,
96+
username: line.username
97+
});
98+
}
99+
return acc;
100+
}, [] as CodespacesUsageItem[]);
101+
102+
usageItems.forEach((item) => {
103+
this.columns.forEach((column: any) => {
104+
if (!(item as any)[column.columnDef]) {
105+
(item as any)[column.columnDef] = 0;
106+
}
107+
});
108+
});
109+
usage = usageItems
110+
this.displayedColumns = this.columns.map(c => c.columnDef);
111+
this.dataSource.data = usage;
112+
}
113+
114+
ngAfterViewInit() {
115+
this.dataSource.paginator = this.paginator;
116+
this.dataSource.sort = this.sort;
117+
}
118+
119+
applyFilter(event: Event) {
120+
const filterValue = (event.target as HTMLInputElement).value;
121+
this.dataSource.filter = filterValue.trim().toLowerCase();
122+
123+
if (this.dataSource.paginator) {
124+
this.dataSource.paginator.firstPage();
125+
}
126+
}
127+
128+
initializeColumns() {
129+
let columns: UsageColumn[] = [];
130+
if (this.tableType === 'sku') {
131+
columns = [
132+
{
133+
columnDef: 'sku',
134+
header: 'Product',
135+
cell: (workflowItem: CodespacesUsageItem) => `${workflowItem.sku}`,
136+
sticky: true
137+
}
138+
];
139+
} else if (this.tableType === 'repo') {
140+
columns = [
141+
{
142+
columnDef: 'repositorySlug',
143+
header: 'Repository',
144+
cell: (workflowItem: CodespacesUsageItem) => `${workflowItem.repositorySlug}`,
145+
sticky: true
146+
}
147+
];
148+
} else if (this.tableType === 'user') {
149+
columns = [
150+
{
151+
columnDef: 'username',
152+
header: 'User',
153+
cell: (workflowItem: CodespacesUsageItem) => `${workflowItem.username}`,
154+
sticky: true
155+
}
156+
];
157+
}
158+
if (this.currency === 'minutes') {
159+
columns.push({
160+
columnDef: 'total',
161+
header: 'Total seats',
162+
cell: (workflowItem: CodespacesUsageItem) => decimalPipe.transform(Math.floor(workflowItem.total)),
163+
footer: () => decimalPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
164+
});
165+
} else if (this.currency === 'cost') {
166+
columns.push({
167+
columnDef: 'cost',
168+
header: 'Total cost',
169+
cell: (workflowItem: CodespacesUsageItem) => currencyPipe.transform(workflowItem.cost),
170+
footer: () => currencyPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
171+
});
172+
}
173+
this.columns = columns;
174+
this.displayedColumns = this.columns.map(c => c.columnDef);
175+
}
176+
}
177+
178+
import { Pipe, PipeTransform } from '@angular/core';
179+
import { CurrencyPipe, DecimalPipe } from '@angular/common';
180+
181+
@Pipe({
182+
name: 'duration'
183+
})
184+
export class DurationPipe implements PipeTransform {
185+
transform(minutes: number): string {
186+
const seconds = minutes * 60;
187+
if (seconds < 60) {
188+
return `${seconds} sec`;
189+
} else if (seconds < 3600) {
190+
return `${Math.round(seconds / 60)} min`;
191+
} else {
192+
return `${Math.round(seconds / 3600)} hr`;
193+
}
194+
}
195+
196+
}
197+
198+
const decimalPipe = new DecimalPipe('en-US');
199+
const currencyPipe = new CurrencyPipe('en-US');

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ <h1 class="mat-headline-2" style="user-select: none; font-weight: 800; text-alig
7979
<app-shared-storage [data]="this.usageLines.sharedStorage" [currency]="currency"></app-shared-storage>
8080
</ng-template>
8181
</mat-tab>
82+
<mat-tab *ngIf="this.usageLines.codespaces.length > 0">
83+
<ng-template mat-tab-label>
84+
<mat-icon class="tab-icon">computer</mat-icon>
85+
Codespaces
86+
</ng-template>
87+
<ng-template matTabContent>
88+
<div style="margin-top: 25px;"></div>
89+
<app-codespaces [data]="this.usageLines.codespaces" [currency]="currency"></app-codespaces>
90+
</ng-template>
91+
</mat-tab>
8292
<mat-tab *ngIf="this.usageLines.copilot.length > 0">
8393
<ng-template mat-tab-label>
8494
<mat-icon class="tab-icon" svgIcon="copilot"></mat-icon>

0 commit comments

Comments
 (0)