diff --git a/src/app/app.component.html b/src/app/app.component.html
index 7a6451c..c7f8a18 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,11 +1,3 @@
diff --git a/src/app/components/usage/actions/actions.component.ts b/src/app/components/usage/actions/actions.component.ts
index 1ae1579..24fb8d6 100644
--- a/src/app/components/usage/actions/actions.component.ts
+++ b/src/app/components/usage/actions/actions.component.ts
@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
-import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
+import { AggregationType, CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
@Component({
selector: 'app-actions',
@@ -9,7 +9,9 @@ import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.
})
export class ActionsComponent implements OnInit {
@Input() data!: CustomUsageReportLine[];
- @Input() currency!: string;
+ @Input() currency!: 'cost' | 'minutes';
+ @Input() grouping!: AggregationType;
+ @Input() filter: string = '';
totalMinutes: number = 0;
totalCost: number = 0;
diff --git a/src/app/components/usage/actions/charts/chart-line-usage-daily/chart-line-usage-daily.component.ts b/src/app/components/usage/actions/charts/chart-line-usage-daily/chart-line-usage-daily.component.ts
index cfe53e0..2a6de46 100644
--- a/src/app/components/usage/actions/charts/chart-line-usage-daily/chart-line-usage-daily.component.ts
+++ b/src/app/components/usage/actions/charts/chart-line-usage-daily/chart-line-usage-daily.component.ts
@@ -1,7 +1,7 @@
import { Component, Input, OnChanges, ViewChild } from '@angular/core';
import * as Highcharts from 'highcharts';
import { ThemingService } from 'src/app/theme.service';
-import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
+import { AggregationType, CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
@Component({
selector: 'app-chart-line-usage-daily',
@@ -51,7 +51,7 @@ export class ChartLineUsageDailyComponent implements OnChanges {
},
};
updateFromInput: boolean = false;
- chartType: 'repo' | 'total' | 'sku' | 'user' | 'workflow' = 'sku';
+ chartType: AggregationType = 'sku';
timeType: 'total' | 'run' | 'daily' | 'weekly' | 'monthly' | 'rolling30' | 'rolling7' = 'rolling30';
rollingDays = 30;
@@ -71,6 +71,7 @@ export class ChartLineUsageDailyComponent implements OnChanges {
(acc, line, index) => {
let name = 'Total';
let timeKey = 'total';
+
if (this.timeType === 'run') {
timeKey = `${line.workflowName}${line.date}${index}`;
} else if (this.timeType === 'daily') {
@@ -78,25 +79,23 @@ export class ChartLineUsageDailyComponent implements OnChanges {
} else if (this.timeType === 'weekly') {
timeKey = this.getWeekOfYear(line.date).toString();
} else if (this.timeType === 'monthly') {
- // get key in format YYYY-MM
timeKey = line.date.toISOString().split('T')[0].slice(0, 7);
} else if (this.timeType.startsWith('rolling')) {
- // get key in format YYYY-MM
timeKey = line.date.toISOString().split('T')[0];
} else if (this.timeType === 'total') {
timeKey = 'total'
}
+
if (this.chartType === 'sku') {
name = this.usageReportService.formatSku(line.sku);
- } else if (this.chartType === 'user') {
+ } else if (this.chartType === 'username') {
name = line.username;
- } else if (this.chartType === 'repo') {
+ } else if (this.chartType === 'repositoryName') {
name = line.repositoryName;
} else if (this.chartType === 'workflow') {
name = line.workflowName;
- } else if (this.chartType === 'total') {
- name = 'total';
}
+
const series = acc.find((s) => s.name === name);
if (series) {
if (!series.data[timeKey]) series.data[timeKey] = [];
@@ -124,6 +123,7 @@ export class ChartLineUsageDailyComponent implements OnChanges {
).sort((a: any, b: any) => {
return b.total - a.total;
}).slice(0, 50);
+
(this.options.series as { name: string; data: [number, number][] }[]) = seriesDays.map((series) => {
let data: [number, number][] = [];
if (this.timeType === 'total') {
@@ -161,7 +161,7 @@ export class ChartLineUsageDailyComponent implements OnChanges {
data
}
});
- if (this.options.legend) this.options.legend.enabled = this.chartType === 'total' ? false : true;
+ // if (this.options.legend) this.options.legend.enabled = this.chartType === 'total' ? false : true;
this.options.yAxis = {
...this.options.yAxis,
title: {
diff --git a/src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.html b/src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.html
index 7f7130d..2edb329 100644
--- a/src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.html
+++ b/src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.html
@@ -1,30 +1,4 @@
-
-
- Grouping
-
- Runner
- Repo
- Workflow
- User
-
-
-
-
- Filter
-
-
-
-
-
@@ -48,7 +22,7 @@
- No data matching the filter "{{input.value}}" |
+ No data matching the filter "{{filter}}" |
diff --git a/src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.ts b/src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.ts
index 62ad84c..046d59f 100644
--- a/src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.ts
+++ b/src/app/components/usage/actions/table-workflow-usage/table-workflow-usage.component.ts
@@ -1,51 +1,9 @@
-import { AfterViewInit, Component, Input, OnChanges, ViewChild } from '@angular/core';
+import { AfterViewInit, Component, Input, OnChanges, ViewChild, Pipe, PipeTransform, SimpleChanges } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
-import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
-
-interface WorkflowUsageItem {
- workflow: string;
- avgTime: number;
- avgCost: number;
- runs: number;
- repo: string;
- total: number;
- cost: number;
- pricePerUnit: number;
- sku: string;
- username: string;
-}
-
-interface RepoUsageItem {
- avgTime: number;
- avgCost: number;
- repo: string;
- runs: number;
- total: number;
- cost: number;
- sku: string;
-}
-
-interface SkuUsageItem {
- avgTime: number;
- avgCost: number;
- sku: string;
- runs: number;
- total: number;
- cost: number;
-}
-
-interface UsageColumn {
- sticky?: boolean;
- columnDef: string;
- header: string;
- cell: (element: any) => any;
- footer?: () => any;
- tooltip?: (element: any) => any;
- icon?: (element: any) => string;
- date?: Date;
-}
+import { UsageReportService, WorkflowUsageItem, RepoUsageItem, SkuUsageItem, UserUsageItem, UsageColumn, AggregatedUsageData, AggregationType, Product } from 'src/app/usage-report.service';
+import { CurrencyPipe, DecimalPipe } from '@angular/common';
@Component({
selector: 'app-table-workflow-usage',
@@ -54,13 +12,15 @@ interface UsageColumn {
standalone: false
})
export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
- columns = [] as UsageColumn[];
+ baseColumns = [] as UsageColumn[];
monthColumns = [] as UsageColumn[];
- displayedColumns = this.columns.map(c => c.columnDef);
- @Input() data!: CustomUsageReportLine[];
- @Input() currency!: string;
- dataSource: MatTableDataSource
= new MatTableDataSource(); // Initialize the dataSource property
- tableType: 'workflow' | 'repo' | 'sku' | 'user' = 'sku';
+ columns = [] as UsageColumn[];
+ displayedColumns: string[] = [];
+ @Input() currency!: 'minutes' | 'cost';
+ @Input() tableType!: AggregationType;
+ @Input() product: Product | Product[] = 'actions';
+ @Input() filter: string = '';
+ dataSource: MatTableDataSource = new MatTableDataSource();
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@@ -69,113 +29,87 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
private usageReportService: UsageReportService,
) { }
- ngOnChanges() {
- this.initializeColumns();
- let usage: WorkflowUsageItem[] | RepoUsageItem[] | SkuUsageItem[] = [];
- let usageItems: WorkflowUsageItem[] = (usage as WorkflowUsageItem[]);
- usageItems = this.data.reduce((acc, line) => {
- const item = acc.find(a => {
- if (this.tableType === 'workflow') {
- return a.workflow === line.workflowName
- } else if (this.tableType === 'repo') {
- return a.repo === line.repositoryName;
- } else if (this.tableType === 'sku') {
- return a.sku === this.usageReportService.formatSku(line.sku);
- } else if (this.tableType === 'user') {
- return a.username === line.username;
- }
- return false
- });
- const month: string = line.date.toLocaleString('default', { month: 'short', year: '2-digit'});
- if (item) {
- if ((item as any)[month]) {
- (item as any)[month] += line.value;
- } else {
- (item as any)[month] = line.value || 0;
- }
- if (!this.columns.find(c => c.columnDef === month)) {
- const column: UsageColumn = {
- columnDef: month,
- header: month,
- cell: (workflowItem: any) => this.currency === 'cost' ? currencyPipe.transform(workflowItem[month]) : decimalPipe.transform(workflowItem[month]),
- footer: () => {
- const total = this.dataSource.data.reduce((acc, item) => acc + (item as any)[month], 0);
- return this.currency === 'cost' ? currencyPipe.transform(total) : decimalPipe.transform(total);
- },
- date: new Date(line.date),
- };
- const lastMonth: string = new Date(line.date.getFullYear(), line.date.getMonth() - 1).toLocaleString('default', { month: 'short' });
- const lastMonthValue = (item as any)[lastMonth];
- if (lastMonthValue) {
- column.tooltip = (workflowItem: WorkflowUsageItem) => {
- return (workflowItem as any)[month + 'PercentChange']?.toFixed(2) + '%';
- };
- column.icon = (workflowItem: WorkflowUsageItem) => {
- const percentageChanged = (workflowItem as any)[month + 'PercentChange'];
- if (percentageChanged > 0) {
- return 'trending_up';
- } else if (percentageChanged < 0) {
- return 'trending_down';
- } else {
- return 'trending_flat';
- }
- };
- }
- this.columns.push(column);
- this.monthColumns.push(column);
- }
- item.cost += line.quantity * line.pricePerUnit;
- item.total += line.quantity;
- item.runs++;
- } else {
- acc.push({
- workflow: line.workflowName,
- repo: line.repositoryName,
- total: line.quantity,
- cost: line.quantity * line.pricePerUnit,
- runs: 1,
- pricePerUnit: line.pricePerUnit || 0,
- avgCost: line.quantity * line.pricePerUnit,
- avgTime: line.value,
- [month]: line.value,
- sku: this.usageReportService.formatSku(line.sku),
- username: line.username,
- });
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['filter'] && !changes['filter'].isFirstChange()) {
+ this.dataSource.filter = this.filter.trim().toLowerCase();
+
+ if (this.dataSource.paginator) {
+ this.dataSource.paginator.firstPage();
}
- return acc;
- }, [] as WorkflowUsageItem[]);
+ return;
+ }
+ this.initializeColumns();
- usageItems.forEach((item) => {
- this.monthColumns.forEach((column: UsageColumn) => {
- const month = column.columnDef;
- if (!(item as any)[month]) {
- (item as any)[month] = 0;
- }
- const lastMonth: string = new Date(new Date().getFullYear(), this.usageReportService.monthsOrder.indexOf(month) - 1).toLocaleString('default', { month: 'short' });
- const lastMonthValue = (item as any)[lastMonth];
- const percentageChanged = this.calculatePercentageChange(lastMonthValue, (item as any)[month]);
- (item as any)[month + 'PercentChange'] = percentageChanged;
+ // Use the service to get aggregated data
+ this.usageReportService.getAggregatedUsageData(this.tableType, this.product).subscribe((aggregatedData: AggregatedUsageData) => {
+ // Create month columns using the service helper method
+ this.monthColumns = this.usageReportService.createMonthColumns(aggregatedData.availableMonths, this.currency, this.dataSource);
+ this.updateMonthColumnFormatting();
+
+ // Combine base columns with month columns and sort them properly
+ const allColumns = [...this.baseColumns, ...this.monthColumns];
+ this.columns = allColumns.sort((a, b) => {
+ // Keep non-date columns first, then sort date columns chronologically
+ if (!a.date && !b.date) return 0;
+ if (!a.date) return -1;
+ if (!b.date) return 1;
+ return a.date.getTime() - b.date.getTime();
});
+
+ this.displayedColumns = this.columns.map(c => c.columnDef);
+ this.dataSource.data = aggregatedData.items;
+ });
+ }
- item.avgTime = item.total / item.runs;
- item.avgCost = item.cost / item.runs;
+ private updateMonthColumnFormatting() {
+ this.monthColumns.forEach(column => {
+ // Update cell formatting based on currency
+ column.cell = (workflowItem: any) =>
+ this.currency === 'cost'
+ ? currencyPipe.transform(workflowItem[column.columnDef])
+ : decimalPipe.transform(workflowItem[column.columnDef]);
+
+ // Update footer formatting
+ const originalFooter = column.footer;
+ column.footer = () => {
+ const total = originalFooter ? originalFooter() : 0;
+ return this.currency === 'cost'
+ ? currencyPipe.transform(total)
+ : decimalPipe.transform(total);
+ };
+
+ // Add tooltip and icon functionality for percentage changes
+ const month = column.columnDef;
+ column.tooltip = (workflowItem: any) => {
+ const percentChange = workflowItem[month + 'PercentChange'];
+ return percentChange !== undefined ? percentChange.toFixed(2) + '%' : '';
+ };
+
+ column.icon = (workflowItem: any) => {
+ const percentageChanged = workflowItem[month + 'PercentChange'];
+ if (percentageChanged > 0) {
+ return 'trending_up';
+ } else if (percentageChanged < 0) {
+ return 'trending_down';
+ } else {
+ return 'trending_flat';
+ }
+ };
});
- usage = usageItems;
- this.columns = this.columns.sort((a, b) => (!a.date || !b.date) ? 0 : a.date.getTime() - b.date.getTime());
- this.displayedColumns = this.columns.map(c => c.columnDef);
- this.dataSource.data = usage;
}
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
const initial = this.dataSource.sortData;
- this.dataSource.sortData = (data: (WorkflowUsageItem | RepoUsageItem | SkuUsageItem)[], sort: MatSort) => {
+ this.dataSource.sortData = (data: (WorkflowUsageItem | RepoUsageItem | SkuUsageItem | UserUsageItem)[], sort: MatSort) => {
switch (sort.active) {
case 'sku':
- return data.sort((a, b) => {
- const orderA = this.usageReportService.skuOrder.indexOf(a.sku);
- const orderB = this.usageReportService.skuOrder.indexOf(b.sku);
+ return data.sort((a, b) => {
+ const skuA = 'sku' in a ? a.sku : '';
+ const skuB = 'sku' in b ? b.sku : '';
+ const orderA = this.usageReportService.skuOrder.indexOf(skuA);
+ const orderB = this.usageReportService.skuOrder.indexOf(skuB);
return sort.direction === 'asc' ? orderA - orderB : orderB - orderA;
});
default:
@@ -184,15 +118,6 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
};
}
- applyFilter(event: Event) {
- const filterValue = (event.target as HTMLInputElement).value;
- this.dataSource.filter = filterValue.trim().toLowerCase();
-
- if (this.dataSource.paginator) {
- this.dataSource.paginator.firstPage();
- }
- }
-
initializeColumns() {
let columns: UsageColumn[] = [];
if (this.tableType === 'workflow') {
@@ -200,13 +125,13 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
{
columnDef: 'workflow',
header: 'Workflow',
- cell: (workflowItem: WorkflowUsageItem) => `${workflowItem.workflow}`,
+ cell: (workflowItem: WorkflowUsageItem) => `${workflowItem.workflowName}`,
sticky: true,
},
{
- columnDef: 'repo',
+ columnDef: 'repositoryName',
header: 'Repository',
- cell: (workflowItem: WorkflowUsageItem) => `${workflowItem.repo}`,
+ cell: (workflowItem: WorkflowUsageItem) => `${workflowItem.repositoryName}`,
},
{
columnDef: 'runner',
@@ -214,12 +139,12 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
cell: (workflowItem: WorkflowUsageItem) => `${workflowItem.sku}`,
},
];
- } else if (this.tableType === 'repo') {
+ } else if (this.tableType === 'repositoryName') {
columns = [
{
- columnDef: 'repo',
- header: 'Source repository',
- cell: (workflowItem: WorkflowUsageItem) => `${workflowItem.repo}`,
+ columnDef: 'repositoryName',
+ header: 'Repository',
+ cell: (workflowItem: WorkflowUsageItem) => `${workflowItem.repositoryName}`,
sticky: true,
},
];
@@ -232,7 +157,7 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
sticky: true,
},
];
- } else if (this.tableType === 'user') {
+ } else if (this.tableType === 'username') {
columns = [
{
columnDef: 'username',
@@ -241,7 +166,35 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
sticky: true,
},
];
+ } else if (this.tableType === 'organization') {
+ columns = [
+ {
+ columnDef: 'organization',
+ header: 'Organization',
+ cell: (workflowItem: WorkflowUsageItem) => workflowItem.organization,
+ sticky: true,
+ },
+ ];
+ } else if (this.tableType === 'costCenterName') {
+ columns = [
+ {
+ columnDef: 'costCenterName',
+ header: 'Cost Center',
+ cell: (workflowItem: WorkflowUsageItem) => workflowItem.costCenterName,
+ sticky: true,
+ },
+ ];
+ } else if (this.tableType === 'workflowPath') {
+ columns = [
+ {
+ columnDef: 'workflowPath',
+ header: 'Workflow Path',
+ cell: (workflowItem: WorkflowUsageItem) => workflowItem.workflowPath,
+ sticky: true,
+ },
+ ];
}
+
columns.push({
columnDef: 'runs',
header: 'Runs',
@@ -256,40 +209,45 @@ export class TableWorkflowUsageComponent implements OnChanges, AfterViewInit {
columnDef: 'avgTime',
header: 'Avg time',
cell: (workflowItem: WorkflowUsageItem) => `${durationPipe.transform(workflowItem.avgTime)}`,
- footer: () => durationPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.avgTime, 0) / this.dataSource.data.length)
+ footer: () => {
+ const avgTime = this.dataSource.data.reduce((acc: number, line: any) => acc + line.avgTime, 0) / this.dataSource.data.length;
+ return durationPipe.transform(avgTime);
+ }
}, {
columnDef: 'total',
header: 'Total',
cell: (workflowItem: WorkflowUsageItem) => decimalPipe.transform(Math.floor(workflowItem.total)),
- footer: () => decimalPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
+ footer: () => {
+ const total = this.dataSource.data.reduce((acc: number, line: any) => acc + line.total, 0);
+ return decimalPipe.transform(total);
+ }
});
} else if (this.currency === 'cost') {
columns.push({
columnDef: 'avgCost',
header: 'Avg run',
cell: (workflowItem: WorkflowUsageItem) => currencyPipe.transform(workflowItem.avgCost),
- footer: () => currencyPipe.transform(this.dataSource.data.reduce((acc, line) => acc += line.cost, 0) / this.dataSource.data.length)
+ footer: () => {
+ const avgCost = this.dataSource.data.reduce((acc: number, line: any) => acc + line.cost, 0) / this.dataSource.data.length;
+ return currencyPipe.transform(avgCost);
+ }
}, {
columnDef: 'cost',
header: 'Total',
cell: (workflowItem: WorkflowUsageItem) => currencyPipe.transform(workflowItem.cost),
- footer: () => currencyPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
+ footer: () => {
+ const total = this.dataSource.data.reduce((acc: number, line: any) => acc + line.cost, 0);
+ return currencyPipe.transform(total);
+ }
});
}
columns[0].footer = () => 'Total';
- this.columns = columns;
- this.monthColumns = [];
- this.displayedColumns = this.columns.map(c => c.columnDef);
- }
-
- calculatePercentageChange(oldValue: number, newValue: number) {
- return (oldValue === 0) ? 0 : ((newValue - oldValue) / oldValue) * 100;
+ this.baseColumns = columns;
+ this.monthColumns = []; // Reset month columns
+ this.displayedColumns = this.baseColumns.map(c => c.columnDef);
}
}
-import { Pipe, PipeTransform } from '@angular/core';
-import { CurrencyPipe, DecimalPipe } from '@angular/common';
-
@Pipe({
name: 'duration',
standalone: false
diff --git a/src/app/components/usage/codespaces/codespaces.component.html b/src/app/components/usage/codespaces/codespaces.component.html
index 500fc1b..96ebaea 100644
--- a/src/app/components/usage/codespaces/codespaces.component.html
+++ b/src/app/components/usage/codespaces/codespaces.component.html
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/app/components/usage/codespaces/codespaces.component.ts b/src/app/components/usage/codespaces/codespaces.component.ts
index 583b24e..e941435 100644
--- a/src/app/components/usage/codespaces/codespaces.component.ts
+++ b/src/app/components/usage/codespaces/codespaces.component.ts
@@ -9,5 +9,5 @@ import { CustomUsageReportLine } from 'src/app/usage-report.service';
})
export class CodespacesComponent {
@Input() data!: CustomUsageReportLine[];
- @Input() currency!: string;
+ @Input() currency!: 'minutes' | 'cost';
}
diff --git a/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage-clean.component.ts b/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage-clean.component.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.html b/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.html
index ea41fad..535df22 100644
--- a/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.html
+++ b/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.html
@@ -1,13 +1,5 @@
-
- Grouping
-
- Product
- Repository
- User
-
-
Filter
diff --git a/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.ts b/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.ts
index df43cf1..10d178f 100644
--- a/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.ts
+++ b/src/app/components/usage/codespaces/table-codespaces-usage/table-codespaces-usage.component.ts
@@ -2,28 +2,10 @@ import { AfterViewInit, Component, Input, OnChanges, ViewChild } from '@angular/
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
-import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
+import { UsageReportService, WorkflowUsageItem, RepoUsageItem, SkuUsageItem, UserUsageItem, UsageColumn, AggregatedUsageData, AggregationType } from 'src/app/usage-report.service';
+import { CurrencyPipe, DecimalPipe } from '@angular/common';
-interface UsageColumn {
- columnDef: string;
- header: string;
- cell: (element: any) => any;
- footer?: () => any;
- sticky?: boolean;
-}
-
-interface CodespacesUsageItem {
- runs: number;
- total: number;
- cost: number;
- pricePerUnit: number;
- owner: string;
- username: string;
- sku: string;
- unitType: string;
- repositorySlug: string;
- sticky?: boolean;
-}
+type Product = 'git_lfs' | 'packages' | 'copilot' | 'actions' | 'codespaces';
@Component({
selector: 'app-table-codespaces-usage',
@@ -33,11 +15,12 @@ interface CodespacesUsageItem {
})
export class TableCodespacesUsageComponent implements OnChanges, AfterViewInit {
columns = [] as UsageColumn[];
- displayedColumns = this.columns.map(c => c.columnDef);
- @Input() data!: CustomUsageReportLine[];
- @Input() currency!: string;
- dataSource: MatTableDataSource = new MatTableDataSource(); // Initialize the dataSource property
- tableType: 'sku' | 'repo' | 'user' = 'sku';
+ monthColumns = [] as UsageColumn[];
+ displayedColumns: string[] = [];
+ @Input() currency!: 'minutes' | 'cost';
+ @Input() tableType!: AggregationType;
+ @Input() product: Product | Product[] = 'codespaces';
+ dataSource: MatTableDataSource = new MatTableDataSource();
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@@ -48,68 +31,54 @@ export class TableCodespacesUsageComponent implements OnChanges, AfterViewInit {
ngOnChanges() {
this.initializeColumns();
- let usage: CodespacesUsageItem[] = [];
- let usageItems: CodespacesUsageItem[] = (usage as CodespacesUsageItem[]);
- usageItems = this.data.reduce((acc, line) => {
- const item = acc.find(a => {
- if (this.tableType === 'sku') {
- return a.sku === line.sku;
- } else if (this.tableType === 'repo') {
- return a.repositorySlug === line.repositoryName;
- } else if (this.tableType === 'user') {
- return a.username === line.username;
- }
- return false;
- });
- const month: string = line.date.toLocaleString('default', { month: 'short' });
- if (item) {
- if ((item as any)[month]) {
- (item as any)[month] += line.value;
- } else {
- (item as any)[month] = line.value || 0;
- }
- item.total += line.value;
- if (!this.columns.find(c => c.columnDef === month)) {
- this.columns.push({
- columnDef: month,
- header: month,
- cell: (workflowItem: any) => this.currency === 'cost' ? currencyPipe.transform(workflowItem[month]) : decimalPipe.transform(workflowItem[month]),
- footer: () => {
- const total = this.dataSource.data.reduce((acc, item) => acc + (item as any)[month], 0);
- return this.currency === 'cost' ? currencyPipe.transform(total) : decimalPipe.transform(total);
- }
- });
- }
- item.cost += line.quantity * line.pricePerUnit;
- item.total += line.quantity;
- item.runs++;
- } else {
- acc.push({
- owner: line.organization,
- total: line.quantity,
- cost: line.quantity * line.pricePerUnit,
- runs: 1,
- pricePerUnit: line.pricePerUnit || 0,
- [month]: line.value,
- sku: line.sku,
- unitType: line.unitType,
- repositorySlug: line.repositoryName,
- username: line.username
- });
- }
- return acc;
- }, [] as CodespacesUsageItem[]);
+
+ // Use the service to get aggregated data
+ this.usageReportService.getAggregatedUsageData(this.tableType, this.product).subscribe((aggregatedData: AggregatedUsageData) => {
+ // Create month columns using the service helper method
+ this.monthColumns = this.usageReportService.createMonthColumns(aggregatedData.availableMonths, this.currency, this.dataSource);
+ this.updateMonthColumnFormatting();
+ this.columns = [...this.columns, ...this.monthColumns];
+ this.columns = this.columns.sort((a, b) => (!a.date || !b.date) ? 0 : a.date.getTime() - b.date.getTime());
+ this.displayedColumns = this.columns.map(c => c.columnDef);
+ this.dataSource.data = aggregatedData.items;
+ });
+ }
+
+ private updateMonthColumnFormatting() {
+ this.monthColumns.forEach(column => {
+ // Update cell formatting based on currency
+ column.cell = (item: any) =>
+ this.currency === 'cost'
+ ? currencyPipe.transform(item[column.columnDef])
+ : decimalPipe.transform(item[column.columnDef]);
+
+ // Update footer formatting
+ const originalFooter = column.footer;
+ column.footer = () => {
+ const total = originalFooter ? originalFooter() : 0;
+ return this.currency === 'cost'
+ ? currencyPipe.transform(total)
+ : decimalPipe.transform(total);
+ };
- usageItems.forEach((item) => {
- this.columns.forEach((column: any) => {
- if (!(item as any)[column.columnDef]) {
- (item as any)[column.columnDef] = 0;
+ // Add tooltip and icon functionality for percentage changes
+ const month = column.columnDef;
+ column.tooltip = (item: any) => {
+ const percentChange = item[month + 'PercentChange'];
+ return percentChange !== undefined ? percentChange.toFixed(2) + '%' : '';
+ };
+
+ column.icon = (item: any) => {
+ const percentageChanged = item[month + 'PercentChange'];
+ if (percentageChanged > 0) {
+ return 'trending_up';
+ } else if (percentageChanged < 0) {
+ return 'trending_down';
+ } else {
+ return 'trending_flat';
}
- });
+ };
});
- usage = usageItems
- this.displayedColumns = this.columns.map(c => c.columnDef);
- this.dataSource.data = usage;
}
ngAfterViewInit() {
@@ -133,51 +102,60 @@ export class TableCodespacesUsageComponent implements OnChanges, AfterViewInit {
{
columnDef: 'sku',
header: 'Product',
- cell: (workflowItem: CodespacesUsageItem) => `${workflowItem.sku}`,
+ cell: (item: SkuUsageItem) => `${item.sku}`,
sticky: true
}
];
- } else if (this.tableType === 'repo') {
+ } else if (this.tableType === 'repositoryName') {
columns = [
{
columnDef: 'repo',
header: 'Repository',
- cell: (workflowItem: CodespacesUsageItem) => `${workflowItem.repositorySlug}`,
+ cell: (item: RepoUsageItem) => `${item.repositoryName}`,
sticky: true
}
];
- } else if (this.tableType === 'user') {
+ } else if (this.tableType === 'username') {
columns = [
{
columnDef: 'username',
header: 'User',
- cell: (workflowItem: CodespacesUsageItem) => `${workflowItem.username}`,
+ cell: (item: UserUsageItem) => `${item.username}`,
sticky: true
}
];
}
+
if (this.currency === 'minutes') {
columns.push({
columnDef: 'total',
- header: 'Total seats',
- cell: (workflowItem: CodespacesUsageItem) => decimalPipe.transform(Math.floor(workflowItem.total)),
- footer: () => decimalPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
+ header: 'Total hours',
+ cell: (item: any) => decimalPipe.transform(Math.floor(item.total)),
+ footer: () => {
+ if (!this.dataSource?.data) return '';
+ const total = this.dataSource.data.reduce((acc: number, item: any) => acc + (item.total || 0), 0);
+ return decimalPipe.transform(total);
+ }
});
} else if (this.currency === 'cost') {
columns.push({
columnDef: 'cost',
header: 'Total cost',
- cell: (workflowItem: CodespacesUsageItem) => currencyPipe.transform(workflowItem.cost),
- footer: () => currencyPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0))
+ cell: (item: any) => currencyPipe.transform(item.cost),
+ footer: () => {
+ if (!this.dataSource?.data) return '';
+ const total = this.dataSource.data.reduce((acc: number, item: any) => acc + (item.cost || 0), 0);
+ return currencyPipe.transform(total);
+ }
});
}
+
this.columns = columns;
this.displayedColumns = this.columns.map(c => c.columnDef);
}
}
import { Pipe, PipeTransform } from '@angular/core';
-import { CurrencyPipe, DecimalPipe } from '@angular/common';
@Pipe({
name: 'duration',
@@ -194,7 +172,6 @@ export class DurationPipe implements PipeTransform {
return `${Math.round(seconds / 3600)} hr`;
}
}
-
}
const decimalPipe = new DecimalPipe('en-US');
diff --git a/src/app/components/usage/copilot/copilot.component.html b/src/app/components/usage/copilot/copilot.component.html
index fe11cc1..e3a3745 100644
--- a/src/app/components/usage/copilot/copilot.component.html
+++ b/src/app/components/usage/copilot/copilot.component.html
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/app/components/usage/copilot/copilot.component.ts b/src/app/components/usage/copilot/copilot.component.ts
index bec29b5..501bb3e 100644
--- a/src/app/components/usage/copilot/copilot.component.ts
+++ b/src/app/components/usage/copilot/copilot.component.ts
@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core';
-import { CustomUsageReportLine } from 'src/app/usage-report.service';
+import { AggregationType, CustomUsageReportLine } from 'src/app/usage-report.service';
@Component({
selector: 'app-copilot',
@@ -9,5 +9,6 @@ import { CustomUsageReportLine } from 'src/app/usage-report.service';
})
export class CopilotComponent {
@Input() data!: CustomUsageReportLine[];
- @Input() currency!: string;
+ @Input() currency!: 'minutes' | 'cost';
+ @Input() grouping!: AggregationType;
}
diff --git a/src/app/components/usage/copilot/table-workflow-usage/table-copilot-usage.component.ts b/src/app/components/usage/copilot/table-workflow-usage/table-copilot-usage.component.ts
index 8427377..1a0b373 100644
--- a/src/app/components/usage/copilot/table-workflow-usage/table-copilot-usage.component.ts
+++ b/src/app/components/usage/copilot/table-workflow-usage/table-copilot-usage.component.ts
@@ -2,24 +2,10 @@ import { AfterViewInit, Component, Input, OnChanges, ViewChild, ChangeDetectorRe
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
-import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
-
-interface UsageColumn {
- columnDef: string;
- header: string;
- cell: (element: any) => any;
- footer?: () => any;
- sticky?: boolean;
-}
+import { UsageReportService, UserUsageItem, UsageColumn, AggregatedUsageData, AggregationType } from 'src/app/usage-report.service';
+import { CurrencyPipe, DecimalPipe } from '@angular/common';
-interface CopilotUsageItem {
- runs: number;
- total: number;
- cost: number;
- pricePerUnit: number;
- owner: string;
- sticky?: boolean;
-}
+type Product = 'git_lfs' | 'packages' | 'copilot' | 'actions' | 'codespaces';
@Component({
selector: 'app-table-copilot-usage',
@@ -29,11 +15,12 @@ interface CopilotUsageItem {
})
export class TableCopilotUsageComponent implements OnChanges, AfterViewInit {
columns = [] as UsageColumn[];
+ monthColumns = [] as UsageColumn[];
displayedColumns: string[] = [];
- @Input() data!: CustomUsageReportLine[];
- @Input() currency!: string;
- dataSource: MatTableDataSource = new MatTableDataSource(); // Initialize the dataSource property
- tableType = 'owner';
+ @Input() currency!: 'minutes' | 'cost';
+ @Input() tableType!: AggregationType;
+ @Input() product: Product | Product[] = 'copilot';
+ dataSource: MatTableDataSource = new MatTableDataSource();
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
@@ -46,91 +33,63 @@ export class TableCopilotUsageComponent implements OnChanges, AfterViewInit {
}
ngOnChanges() {
- if (!this.data) {
- return; // Avoid processing if data is not available yet
- }
-
this.initializeColumns();
- let usage: CopilotUsageItem[] = [];
- let usageItems: CopilotUsageItem[] = (usage as CopilotUsageItem[]);
- usageItems = this.data.reduce((acc, line) => {
- const item = acc.find(a => {
- if (this.tableType === 'owner') {
- return a.owner === line.organization;
- }
- return false;
- });
- const month: string = line.date.toLocaleString('default', { month: 'short' });
- if (item) {
- if ((item as any)[month]) {
- (item as any)[month] += line.value;
- } else {
- (item as any)[month] = line.value || 0;
- }
- item.total += line.value;
- if (!this.columns.find(c => c.columnDef === month)) {
- this.columns.push({
- columnDef: month,
- header: month,
- cell: (workflowItem: any) => this.currency === 'cost' ? currencyPipe.transform(workflowItem[month]) : decimalPipe.transform(workflowItem[month]),
- footer: () => {
- if (!this.dataSource?.data) return '';
- const total = this.dataSource.data.reduce((acc, item) => acc + (item as any)[month], 0);
- return this.currency === 'cost' ? currencyPipe.transform(total) : decimalPipe.transform(total);
- }
- });
- }
- item.cost += line.quantity * line.pricePerUnit;
- item.total += line.quantity;
- item.runs++;
- } else {
- acc.push({
- owner: line.organization,
- total: line.quantity,
- cost: line.quantity * line.pricePerUnit,
- runs: 1,
- pricePerUnit: line.pricePerUnit || 0,
- [month]: line.value,
- });
- }
- return acc;
- }, [] as CopilotUsageItem[]);
+
+ // Use the service to get aggregated data
+ this.usageReportService.getAggregatedUsageData(this.tableType, this.product).subscribe((aggregatedData: AggregatedUsageData) => {
+ // Create month columns using the service helper method
+ this.monthColumns = this.usageReportService.createMonthColumns(aggregatedData.availableMonths, this.currency, this.dataSource);
+ this.updateMonthColumnFormatting();
+ this.columns = [...this.columns, ...this.monthColumns];
+ this.columns = this.columns.sort((a, b) => (!a.date || !b.date) ? 0 : a.date.getTime() - b.date.getTime());
+ this.displayedColumns = this.columns.map(c => c.columnDef);
+ this.dataSource.data = aggregatedData.items as UserUsageItem[];
+
+ // Mark for check to ensure proper change detection
+ this.cdr.markForCheck();
+ });
+ }
+
+ private updateMonthColumnFormatting() {
+ this.monthColumns.forEach(column => {
+ // Update cell formatting based on currency
+ column.cell = (item: any) =>
+ this.currency === 'cost'
+ ? currencyPipe.transform(item[column.columnDef])
+ : decimalPipe.transform(item[column.columnDef]);
+
+ // Update footer formatting
+ const originalFooter = column.footer;
+ column.footer = () => {
+ const total = originalFooter ? originalFooter() : 0;
+ return this.currency === 'cost'
+ ? currencyPipe.transform(total)
+ : decimalPipe.transform(total);
+ };
- usageItems.forEach((item) => {
- this.columns.forEach((column: any) => {
- if (!(item as any)[column.columnDef]) {
- (item as any)[column.columnDef] = 0;
+ // Add tooltip and icon functionality for percentage changes
+ const month = column.columnDef;
+ column.tooltip = (item: any) => {
+ const percentChange = item[month + 'PercentChange'];
+ return percentChange !== undefined ? percentChange.toFixed(2) + '%' : '';
+ };
+
+ column.icon = (item: any) => {
+ const percentageChanged = item[month + 'PercentChange'];
+ if (percentageChanged > 0) {
+ return 'trending_up';
+ } else if (percentageChanged < 0) {
+ return 'trending_down';
+ } else {
+ return 'trending_flat';
}
- });
+ };
});
- usage = usageItems;
-
- // Update displayedColumns first
- this.displayedColumns = this.columns.map(c => c.columnDef);
-
- // Then update the data source
- this.dataSource = new MatTableDataSource(usage);
-
- // Apply sort and pagination immediately, without setTimeout
- if (this.sort) {
- this.dataSource.sort = this.sort;
- }
- if (this.paginator) {
- this.dataSource.paginator = this.paginator;
- }
-
- // Mark for check to ensure proper change detection
- this.cdr.markForCheck();
}
ngAfterViewInit() {
- // We use next tick to avoid the ExpressionChangedAfterItHasBeenCheckedError
- Promise.resolve().then(() => {
- if (this.dataSource) {
- this.dataSource.paginator = this.paginator;
- this.dataSource.sort = this.sort;
- }
- });
+ this.dataSource.paginator = this.paginator;
+ this.dataSource.sort = this.sort;
}
applyFilter(event: Event) {
@@ -144,36 +103,37 @@ export class TableCopilotUsageComponent implements OnChanges, AfterViewInit {
initializeColumns() {
let columns: UsageColumn[] = [];
- if (this.tableType === 'owner') {
- columns = [
- {
- columnDef: 'owner',
- header: 'Owner',
- cell: (workflowItem: CopilotUsageItem) => `${workflowItem.owner}`,
- sticky: true
- }
- ];
- if (this.currency === 'minutes') {
- columns.push({
- columnDef: 'total',
- header: 'Total seats',
- cell: (workflowItem: CopilotUsageItem) => decimalPipe.transform(Math.floor(workflowItem.total)),
- footer: () => {
- if (!this.data) return '';
- return decimalPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0));
- }
- });
- } else if (this.currency === 'cost') {
- columns.push({
- columnDef: 'cost',
- header: 'Total cost',
- cell: (workflowItem: CopilotUsageItem) => currencyPipe.transform(workflowItem.cost),
- footer: () => {
- if (!this.data) return '';
- return currencyPipe.transform(this.data.reduce((acc, line) => acc += line.value, 0));
- }
- });
+
+ // Add the username column for user aggregation
+ columns = [
+ {
+ columnDef: 'username',
+ header: 'User',
+ cell: (item: UserUsageItem) => `${item.username}`,
+ sticky: true
}
+ ];
+
+ if (this.currency === 'minutes') {
+ columns.push({
+ columnDef: 'total',
+ header: 'Total seats',
+ cell: (item: UserUsageItem) => decimalPipe.transform(Math.floor(item.total)),
+ footer: () => {
+ if (!this.dataSource?.data) return '';
+ return decimalPipe.transform(this.dataSource.data.reduce((acc, item) => acc + item.total, 0));
+ }
+ });
+ } else if (this.currency === 'cost') {
+ columns.push({
+ columnDef: 'cost',
+ header: 'Total cost',
+ cell: (item: UserUsageItem) => currencyPipe.transform(item.cost),
+ footer: () => {
+ if (!this.dataSource?.data) return '';
+ return currencyPipe.transform(this.dataSource.data.reduce((acc, item) => acc + item.cost, 0));
+ }
+ });
}
// Important: Clear columns before setting new ones
@@ -187,26 +147,5 @@ export class TableCopilotUsageComponent implements OnChanges, AfterViewInit {
}
}
-import { Pipe, PipeTransform } from '@angular/core';
-import { CurrencyPipe, DecimalPipe } from '@angular/common';
-
-@Pipe({
- name: 'duration',
- standalone: false
-})
-export class DurationPipe implements PipeTransform {
- transform(minutes: number): string {
- const seconds = minutes * 60;
- if (seconds < 60) {
- return `${seconds} sec`;
- } else if (seconds < 3600) {
- return `${Math.round(seconds / 60)} min`;
- } else {
- return `${Math.round(seconds / 3600)} hr`;
- }
- }
-
-}
-
const decimalPipe = new DecimalPipe('en-US');
const currencyPipe = new CurrencyPipe('en-US');
\ No newline at end of file
diff --git a/src/app/components/usage/shared-storage/shared-storage.component.ts b/src/app/components/usage/shared-storage/shared-storage.component.ts
index efea970..d813049 100644
--- a/src/app/components/usage/shared-storage/shared-storage.component.ts
+++ b/src/app/components/usage/shared-storage/shared-storage.component.ts
@@ -9,5 +9,5 @@ import { CustomUsageReportLine } from 'src/app/usage-report.service';
})
export class SharedStorageComponent {
@Input() data!: CustomUsageReportLine[];
- @Input() currency!: string;
+ @Input() currency!: 'cost' | 'minutes';
}
diff --git a/src/app/components/usage/shared-storage/table-shared-storage/table-shared-storage.component.ts b/src/app/components/usage/shared-storage/table-shared-storage/table-shared-storage.component.ts
index c0422e2..cb8b60c 100644
--- a/src/app/components/usage/shared-storage/table-shared-storage/table-shared-storage.component.ts
+++ b/src/app/components/usage/shared-storage/table-shared-storage/table-shared-storage.component.ts
@@ -1,21 +1,10 @@
-import { Component, Input, Pipe, PipeTransform, ViewChild, OnChanges, AfterViewInit, ChangeDetectorRef } from '@angular/core';
+import { Component, Input, Pipe, PipeTransform, ViewChild, OnChanges, AfterViewInit, ChangeDetectorRef, SimpleChanges } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
-import { CustomUsageReportLine } from 'src/app/usage-report.service';
+import { AggregatedUsageData, AggregationType, CustomUsageReportLine, Product, RepoUsageItem, SkuUsageItem, UsageColumn, UsageReportService, UserUsageItem, WorkflowUsageItem } from 'src/app/usage-report.service';
import { CurrencyPipe } from '@angular/common';
-type SharedStorageUsageItem = {
- repo: string;
- count: number;
- avgSize: number;
- total: number;
- totalCost: number;
- avgCost: number;
- pricePerUnit: number;
- costPerDay: number;
-};
-
@Component({
selector: 'app-table-shared-storage',
templateUrl: './table-shared-storage.component.html',
@@ -23,101 +12,134 @@ type SharedStorageUsageItem = {
standalone: false
})
export class TableSharedStorageComponent implements OnChanges, AfterViewInit {
- columns: {
- columnDef: string;
- header: string;
- cell: (element: SharedStorageUsageItem) => any;
- footer?: () => any;
- sticky?: boolean;
- }[] = [];
displayedColumns: string[] = [];
@Input() data!: CustomUsageReportLine[];
- @Input() currency!: string;
- dataSource: MatTableDataSource = new MatTableDataSource();
+ @Input() tableType: AggregationType = 'repositoryName';
+ @Input() currency!: 'minutes' | 'cost';
+ @Input() product: Product | Product[] = 'actions';
+ dataSource: MatTableDataSource = new MatTableDataSource();
+ baseColumns = [] as UsageColumn[];
+ monthColumns = [] as UsageColumn[];
+ columns = [] as UsageColumn[];
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
- constructor(private cdr: ChangeDetectorRef) {
- this.initializeColumns();
- }
+ constructor(
+ private usageReportService: UsageReportService,
+ private cdr: ChangeDetectorRef
+ ) { }
- ngOnChanges() {
- if (!this.data) {
- return; // Avoid processing if data is not available yet
- }
+ ngOnChanges(changes: SimpleChanges) {
+ // if (changes['filter'] && !changes['filter'].isFirstChange()) {
+ // this.dataSource.filter = this.filter.trim().toLowerCase();
- this.initializeColumns();
+ // if (this.dataSource.paginator) {
+ // this.dataSource.paginator.firstPage();
+ // }
+ // return;
+ // }
+ this.initializeColumns();
- const workflowUsage = this.data.reduce((acc, line) => {
- const workflowEntry = acc.find(a => a.repo === line.repositoryName);
- const date = line.date;
- const month: string = date.toLocaleString('default', { month: 'short' });
- const cost = line.quantity * line.pricePerUnit;
- if (workflowEntry) {
- if ((workflowEntry as any)[month] as any) {
- (workflowEntry as any)[month] += line.value;
- } else {
- (workflowEntry as any)[month] = line.value;
- }
- if (!this.columns.find(c => c.columnDef === month)) {
- this.columns.push({
- columnDef: month,
- header: month,
- cell: (sharedStorageItem: any) => this.currency === 'cost' ? currencyPipe.transform(sharedStorageItem[month]) : fileSizePipe.transform(sharedStorageItem[month]),
- footer: () => {
- if (!this.dataSource?.data) return '';
- const total = this.dataSource.data.reduce((acc, item) => acc + (item as any)[month], 0);
- return this.currency === 'cost' ? currencyPipe.transform(total) : fileSizePipe.transform(total);
- }
+ // Use the service to get aggregated data
+ this.usageReportService.getAggregatedUsageData(this.tableType, this.product).subscribe((aggregatedData: AggregatedUsageData) => {
+ // Create month columns using the service helper method
+ this.monthColumns = this.usageReportService.createMonthColumns(aggregatedData.availableMonths, this.currency, this.dataSource);
+ // this.updateMonthColumnFormatting();
+
+ // Combine base columns with month columns and sort them properly
+ const allColumns = [...this.baseColumns, ...this.monthColumns];
+ this.columns = allColumns.sort((a, b) => {
+ // Keep non-date columns first, then sort date columns chronologically
+ if (!a.date && !b.date) return 0;
+ if (!a.date) return -1;
+ if (!b.date) return 1;
+ return a.date.getTime() - b.date.getTime();
});
- }
- workflowEntry.total += line.quantity;
- workflowEntry.totalCost += cost;
- workflowEntry.count++;
- workflowEntry.costPerDay = cost;
- } else {
- acc.push({
- repo: line.repositoryName,
- total: line.quantity,
- count: 1,
- totalCost: cost,
- avgSize: 0,
- avgCost: 0,
- [month]: line.value,
- pricePerUnit: line.pricePerUnit,
- costPerDay: cost
+
+ this.displayedColumns = this.columns.map(c => c.columnDef);
+ this.dataSource.data = aggregatedData.items;
});
- }
- return acc;
- }, [] as SharedStorageUsageItem[]);
- workflowUsage.forEach((sharedStorageItem: SharedStorageUsageItem) => {
- this.columns.forEach((column) => {
- if (!(sharedStorageItem as any)[column.columnDef]) {
- (sharedStorageItem as any)[column.columnDef] = 0;
- }
- sharedStorageItem.avgSize = sharedStorageItem.total / sharedStorageItem.count;
- sharedStorageItem.avgCost = sharedStorageItem.totalCost / sharedStorageItem.count;
- });
- });
+ return;
+ // if (!this.data) {
+ // return; // Avoid processing if data is not available yet
+ // }
+
+ // this.initializeColumns();
+
+ // const workflowUsage = this.data.reduce((acc, line) => {
+ // const workflowEntry = acc.find(a => a.repositoryName === line.repositoryName);
+ // const date = line.date;
+ // const month: string = date.toLocaleString('default', { month: 'short' });
+ // const cost = line.quantity * line.pricePerUnit;
+ // if (workflowEntry) {
+ // if ((workflowEntry as any)[month] as any) {
+ // (workflowEntry as any)[month] += line.value;
+ // } else {
+ // (workflowEntry as any)[month] = line.value;
+ // }
+ // if (!this.columns.find(c => c.columnDef === month)) {
+ // this.columns.push({
+ // columnDef: month,
+ // header: month,
+ // cell: (sharedStorageItem: WorkflowUsageItem) => this.currency === 'cost' ? currencyPipe.transform(sharedStorageItem[month]) : fileSizePipe.transform(sharedStorageItem[month]),
+ // footer: () => {
+ // if (!this.dataSource?.data) return '';
+ // const total = this.dataSource.data.reduce((acc, item) => acc + (item as any)[month], 0);
+ // return this.currency === 'cost' ? currencyPipe.transform(total) : fileSizePipe.transform(total);
+ // }
+ // });
+ // }
+ // workflowEntry.total += line.quantity;
+ // workflowEntry.totalCost += cost;
+ // workflowEntry.count++;
+ // workflowEntry.costPerDay = cost;
+ // } else {
+ // acc.push({
+ // repositoryName: line.repositoryName,
+ // total: line.quantity,
+ // count: 1,
+ // totalCost: cost,
+ // avgSize: 0,
+ // avgCost: 0,
+ // [month]: line.value,
+ // pricePerUnit: line.pricePerUnit,
+ // costPerDay: cost,
+ // avgTime: 0,
+ // workflowPath: line.workflowPath || '',
+ // date: line.date
+ // });
+ // }
+ // return acc;
+ // }, [] as WorkflowUsageItem[]);
- // Update displayedColumns first
- this.displayedColumns = this.columns.map(c => c.columnDef);
+ // workflowUsage.forEach((sharedStorageItem: WorkflowUsageItem) => {
+ // this.columns.forEach((column) => {
+ // if (!(sharedStorageItem as any)[column.columnDef]) {
+ // (sharedStorageItem as any)[column.columnDef] = 0;
+ // }
+ // sharedStorageItem.avgSize = sharedStorageItem.total / sharedStorageItem.count;
+ // sharedStorageItem.avgCost = sharedStorageItem.totalCost / sharedStorageItem.count;
+ // });
+ // });
+
+ // // Update displayedColumns first
+ // this.displayedColumns = this.columns.map(c => c.columnDef);
- // Then update the data source
- this.dataSource = new MatTableDataSource(workflowUsage);
+ // // Then update the data source
+ // this.dataSource = new MatTableDataSource(workflowUsage);
- // Apply sort and pagination immediately, without setTimeout
- if (this.sort) {
- this.dataSource.sort = this.sort;
- }
- if (this.paginator) {
- this.dataSource.paginator = this.paginator;
- }
+ // // Apply sort and pagination immediately, without setTimeout
+ // if (this.sort) {
+ // this.dataSource.sort = this.sort;
+ // }
+ // if (this.paginator) {
+ // this.dataSource.paginator = this.paginator;
+ // }
- // Mark for check to ensure proper change detection
- this.cdr.markForCheck();
+ // // Mark for check to ensure proper change detection
+ // this.cdr.markForCheck();
}
ngAfterViewInit() {
@@ -134,21 +156,21 @@ export class TableSharedStorageComponent implements OnChanges, AfterViewInit {
const columns: {
columnDef: string,
header: string,
- cell: (sharedStorageItem: SharedStorageUsageItem) => any,
+ cell: (sharedStorageItem: WorkflowUsageItem) => any,
footer?: () => any,
sticky?: boolean
}[] = [
{
- columnDef: 'repo',
+ columnDef: 'repositoryName',
header: 'Repository',
- cell: (sharedStorageItem: any) => sharedStorageItem.repo,
+ cell: (sharedStorageItem: WorkflowUsageItem) => sharedStorageItem.repositoryName,
footer: () => 'Total',
sticky: true
},
{
columnDef: 'count',
header: 'Count',
- cell: (sharedStorageItem: any) => sharedStorageItem.count,
+ cell: (sharedStorageItem: WorkflowUsageItem) => sharedStorageItem.count,
footer: () => {
if (!this.dataSource?.data) return '';
return this.dataSource.data.reduce((acc, line) => acc + line.count, 0);
@@ -160,16 +182,16 @@ export class TableSharedStorageComponent implements OnChanges, AfterViewInit {
{
columnDef: 'total',
header: 'Total Cost',
- cell: (sharedStorageItem: any) => currencyPipe.transform(sharedStorageItem.totalCost),
+ cell: (sharedStorageItem: WorkflowUsageItem) => currencyPipe.transform(sharedStorageItem.total),
footer: () => {
if (!this.dataSource?.data) return '';
- return currencyPipe.transform(this.dataSource.data.reduce((acc, line) => acc + line.totalCost, 0));
+ return currencyPipe.transform(this.dataSource.data.reduce((acc, line) => acc + line.total, 0));
}
},
{
columnDef: 'costPerDay',
header: 'Cost Per Day',
- cell: (sharedStorageItem: SharedStorageUsageItem) => currencyPipe.transform(sharedStorageItem.costPerDay),
+ cell: (sharedStorageItem: WorkflowUsageItem) => currencyPipe.transform(sharedStorageItem.costPerDay),
footer: () => {
if (!this.dataSource?.data) return '';
return currencyPipe.transform(this.dataSource.data.reduce((acc, line) => acc + line.costPerDay, 0));
@@ -181,7 +203,7 @@ export class TableSharedStorageComponent implements OnChanges, AfterViewInit {
{
columnDef: 'avgSize',
header: 'Average Size',
- cell: (sharedStorageItem: any) => fileSizePipe.transform(sharedStorageItem.avgSize),
+ cell: (sharedStorageItem: WorkflowUsageItem) => fileSizePipe.transform(sharedStorageItem.avgSize),
footer: () => {
if (!this.dataSource?.data || this.dataSource.data.length === 0) return '';
return fileSizePipe.transform(this.dataSource.data.reduce((acc, line) => acc + line.avgSize, 0) / this.dataSource.data.length);
@@ -190,7 +212,7 @@ export class TableSharedStorageComponent implements OnChanges, AfterViewInit {
{
columnDef: 'total',
header: 'Total',
- cell: (sharedStorageItem: any) => fileSizePipe.transform(sharedStorageItem.total),
+ cell: (sharedStorageItem: WorkflowUsageItem) => fileSizePipe.transform(sharedStorageItem.total),
footer: () => {
if (!this.dataSource?.data) return '';
return fileSizePipe.transform(this.dataSource.data.reduce((acc, line) => acc + line.total, 0));
@@ -198,7 +220,8 @@ export class TableSharedStorageComponent implements OnChanges, AfterViewInit {
}
);
}
- this.columns = columns;
+ this.baseColumns = columns;
+ this.monthColumns = []; // Reset month columns
// Update displayedColumns immediately after updating columns
this.displayedColumns = this.columns.map(c => c.columnDef);
}
diff --git a/src/app/components/usage/usage.component.html b/src/app/components/usage/usage.component.html
index 285a1fd..3a70054 100644
--- a/src/app/components/usage/usage.component.html
+++ b/src/app/components/usage/usage.component.html
@@ -1,24 +1,53 @@
-
+
+
GitHub
Usage Report Viewer
-
-
+
-
-
= 99" mode="indeterminate">
+
+
= 99" mode="indeterminate">
+
+
Premium Requests
0">
@@ -68,7 +93,8 @@ Premium Requests
-
+
0">
@@ -98,7 +124,8 @@ Premium Requests
-
+
@@ -128,4 +155,4 @@
Premium Requests
-
+
\ No newline at end of file
diff --git a/src/app/components/usage/usage.component.scss b/src/app/components/usage/usage.component.scss
index d6267cc..438742e 100644
--- a/src/app/components/usage/usage.component.scss
+++ b/src/app/components/usage/usage.component.scss
@@ -10,10 +10,13 @@ form {
top: 0;
z-index: 999;
display: flex;
+ align-items: center;
+ background: var(--mat-sys-background);
+ box-sizing: border-box;
+ padding: 10px 0;
mat-form-field {
margin-right: 15px;
}
- margin-top: 15px;;
@media screen and (max-width: 767px) {
flex-wrap: wrap;
}
diff --git a/src/app/components/usage/usage.component.ts b/src/app/components/usage/usage.component.ts
index 4952da4..cdcba70 100644
--- a/src/app/components/usage/usage.component.ts
+++ b/src/app/components/usage/usage.component.ts
@@ -1,11 +1,12 @@
-import { OnInit, ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
+import { OnInit, ChangeDetectorRef, Component, OnDestroy, inject, Input } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { UsageReport } from 'github-usage-report/src/types';
import { Observable, Subscription, debounceTime, map, startWith } from 'rxjs';
-import { CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
+import { AggregationType, CustomUsageReportLine, UsageReportService } from 'src/app/usage-report.service';
import { DialogBillingNavigateComponent } from './dialog-billing-navigate';
import { MatDialog } from '@angular/material/dialog';
import { ModelUsageReport } from 'github-usage-report';
+import { MatBottomSheet, MatBottomSheetRef } from '@angular/material/bottom-sheet';
@Component({
selector: 'app-usage',
@@ -14,6 +15,7 @@ import { ModelUsageReport } from 'github-usage-report';
standalone: false
})
export class UsageComponent implements OnInit, OnDestroy {
+ @Input() theme!: 'light-theme' | 'dark-theme';
usage!: UsageReport;
usageCopilotPremiumRequests!: ModelUsageReport;
usageLines = {} as {
@@ -28,15 +30,15 @@ export class UsageComponent implements OnInit, OnDestroy {
});
minDate!: Date;
maxDate!: Date;
- workflows: string[] = [];
workflow!: string;
- _filteredWorkflows!: Observable;
- workflowControl = new FormControl('');
status: string = 'Usage Report';
progress: number | null = null;
subscriptions: Subscription[] = [];
+ filter: string = '';
currency: 'minutes' | 'cost' = 'cost';
tabSelected: 'shared-storage' | 'copilot' | 'actions' = 'actions';
+ groupingControl = new FormControl('sku' as AggregationType);
+ private _bottomSheet = inject(MatBottomSheet);
constructor(
private usageReportService: UsageReportService,
@@ -58,19 +60,6 @@ export class UsageComponent implements OnInit, OnDestroy {
})
);
- this.subscriptions.push(
- this.workflowControl.valueChanges.subscribe(value => {
- if (!value || value === '') value = '';
- this.usageReportService.applyFilter({
- workflow: value,
- });
- })
- );
- this._filteredWorkflows = this.workflowControl.valueChanges.pipe(
- startWith(''),
- map(value => this._filterWorkflows(value || '')),
- );
-
this.subscriptions.push(
this.usageReportService.getUsageFilteredByProduct('actions').subscribe((usageLines) => {
this.usageLines.actions = usageLines;
@@ -83,10 +72,7 @@ export class UsageComponent implements OnInit, OnDestroy {
}),
this.usageReportService.getUsageFilteredByProduct('codespaces').subscribe((usageLines) => {
this.usageLines.codespaces = usageLines;
- }),
- this.usageReportService.getWorkflowsFiltered().subscribe((workflows) => {
- this.workflows = workflows;
- }),
+ })
);
}
@@ -129,11 +115,6 @@ export class UsageComponent implements OnInit, OnDestroy {
this.cdr.detectChanges();
}
- private _filterWorkflows(workflow: string): string[] {
- const filterValue = workflow.toLowerCase();
- return this.workflows.filter(option => option.toLowerCase().includes(filterValue));
- }
-
navigateToBilling() {
const dialogRef = this.dialog.open(DialogBillingNavigateComponent);
@@ -148,6 +129,11 @@ export class UsageComponent implements OnInit, OnDestroy {
});
}
+ applyFilter(event: Event) {
+ const filterValue = (event.target as HTMLInputElement).value;
+ this.filter = filterValue.trim().toLowerCase();
+ }
+
changeCurrency(currency: string) {
this.currency = currency as 'minutes' | 'cost';
this.usageReportService.setValueType(this.currency);
@@ -193,4 +179,76 @@ export class UsageComponent implements OnInit, OnDestroy {
a.click();
(a as any).parentNode.removeChild(a);
}
+
+ openBottomSheet(): void {
+ this._bottomSheet.open(BottomSheetOverviewExampleSheetComponent, {
+ autoFocus: false
+ });
+ }
+
+ reloadPage() {
+ window.location.reload();
+ }
}
+
+@Component({
+ selector: 'app-bottom-sheet-overview-example-sheet',
+ standalone: false,
+ // templateUrl: 'bottom-sheet-overview-example-sheet.html',
+ template: `
+
+ Workflow
+
+
+ @for (option of _filteredWorkflows | async; track option) {
+ {{option}}
+ }
+
+ @if (workflowControl.value) {
+
+ }
+
+ `
+})
+export class BottomSheetOverviewExampleSheetComponent {
+ @Input() tabSelected: 'shared-storage' | 'copilot' | 'actions' = 'actions';
+ private _bottomSheetRef =
+ inject>(MatBottomSheetRef);
+
+ workflowControl = new FormControl('');
+ _filteredWorkflows!: Observable;
+ workflows: string[] = [];
+
+ constructor (
+ private usageReportService: UsageReportService,
+ ) {}
+
+ ngOnInit() {
+ this.usageReportService.getWorkflowsFiltered().subscribe((workflows) => {
+ this.workflows = workflows;
+ });
+ this.workflowControl.valueChanges.subscribe(value => {
+ if (!value || value === '') value = '';
+ this.usageReportService.applyFilter({
+ workflow: value,
+ });
+ })
+ this._filteredWorkflows = this.workflowControl.valueChanges.pipe(
+ startWith(''),
+ map(value => this._filterWorkflows(value || '')),
+ );
+ }
+
+ private _filterWorkflows(workflow: string): string[] {
+ const filterValue = workflow.toLowerCase();
+ return this.workflows.filter(option => option.toLowerCase().includes(filterValue));
+ }
+
+ openLink(event: MouseEvent): void {
+ this._bottomSheetRef.dismiss();
+ event.preventDefault();
+ }
+}
\ No newline at end of file
diff --git a/src/app/usage-report.service.ts b/src/app/usage-report.service.ts
index e28fd47..e1d27b9 100644
--- a/src/app/usage-report.service.ts
+++ b/src/app/usage-report.service.ts
@@ -1,7 +1,9 @@
import { TitleCasePipe } from '@angular/common';
import { Injectable } from '@angular/core';
import { ModelUsageReport, readGithubUsageReport, readModelUsageReport, UsageReport, UsageReportLine } from 'github-usage-report';
-import { BehaviorSubject, Observable, map } from 'rxjs';
+import { BehaviorSubject, Observable, map, tap } from 'rxjs';
+
+const titlecasePipe = new TitleCasePipe();
interface Filter {
startDate: Date;
@@ -10,8 +12,6 @@ interface Filter {
sku: string;
}
-type Product = 'git_lfs' | 'packages' | 'copilot' | 'actions' | 'codespaces';
-
export interface CustomUsageReportLine extends UsageReportLine {
value: number;
}
@@ -20,6 +20,88 @@ export interface CustomUsageReport extends UsageReport {
lines: CustomUsageReportLine[];
}
+export interface SharedStorageUsageItem {
+ repositoryName: string;
+ count: number;
+ avgSize: number;
+ total: number;
+ totalCost: number;
+ avgCost: number;
+ pricePerUnit: number;
+ costPerDay: number;
+}
+
+export interface WorkflowUsageItem {
+ workflowName: string;
+ workflowPath: string;
+ costCenterName: string;
+ organization: string;
+ avgTime: number;
+ avgCost: number;
+ runs: number;
+ repositoryName: string;
+ total: number;
+ cost: number;
+ pricePerUnit: number;
+ sku: string;
+ username: string;
+ [month: string]: any; // For dynamic month columns
+ avgSize: number; // For shared storage
+ costPerDay: number; // For shared storage
+ count: number; // For shared storage
+}
+
+export interface RepoUsageItem {
+ avgTime: number;
+ avgCost: number;
+ repositoryName: string;
+ runs: number;
+ total: number;
+ cost: number;
+ sku: string;
+ [month: string]: any; // For dynamic month columns
+}
+
+export interface SkuUsageItem {
+ avgTime: number;
+ avgCost: number;
+ sku: string;
+ runs: number;
+ total: number;
+ cost: number;
+ [month: string]: any; // For dynamic month columns
+}
+
+export interface UserUsageItem {
+ avgTime: number;
+ avgCost: number;
+ username: string;
+ runs: number;
+ total: number;
+ cost: number;
+ [month: string]: any; // For dynamic month columns
+}
+
+export interface UsageColumn {
+ sticky?: boolean;
+ columnDef: string;
+ header: string;
+ cell: (element: any) => any;
+ footer?: () => any;
+ tooltip?: (element: any) => any;
+ icon?: (element: any) => string;
+ date?: Date;
+}
+
+export interface AggregatedUsageData {
+ items: WorkflowUsageItem[] | RepoUsageItem[] | SkuUsageItem[] | UserUsageItem[];
+ availableMonths: string[]; // Just the month keys, let components build columns
+ dateRange: { start: Date; end: Date };
+}
+
+export type AggregationType = 'workflow' | 'sku' | 'organization' | 'repositoryName' | 'costCenterName' | 'username' | 'workflowPath';
+export type Product = 'git_lfs' | 'packages' | 'copilot' | 'actions' | 'codespaces';
+
@Injectable({
providedIn: 'root'
})
@@ -30,6 +112,7 @@ export class UsageReportService {
usageReportCopilotPremiumRequests!: ModelUsageReport;
usageReportFiltered: BehaviorSubject = new BehaviorSubject([]);
usageReportFilteredProduct: { [key: string]: Observable } = {};
+
filters: Filter = {
startDate: new Date(),
endDate: new Date(),
@@ -147,6 +230,7 @@ export class UsageReportService {
this.usageReportPremiumRequestsData = usageReportData;
await readModelUsageReport(this.usageReportPremiumRequestsData).then((report) => {
this.usageReportCopilotPremiumRequests = report;
+ cb?.(this.usageReport, 100);
});
return this.usageReportCopilotPremiumRequests;
}
@@ -154,6 +238,7 @@ export class UsageReportService {
async setUsageReportData(usageReportData: string, cb?: (usageReport: CustomUsageReport, percent: number) => void): Promise {
this.usageReportData = usageReportData;
this.usageReport = await readGithubUsageReport(this.usageReportData) as CustomUsageReport;
+
cb?.(this.usageReport, 100);
this.filters.startDate = this.usageReport.startDate;
this.filters.endDate = this.usageReport.endDate;
@@ -184,7 +269,6 @@ export class UsageReportService {
}
});
this.setValueType(this.valueType.value);
- console.log('Usage Report Loaded:', this.usageReport);
return this.usageReport;
}
@@ -195,6 +279,7 @@ export class UsageReportService {
sku?: string,
}): void {
Object.assign(this.filters, filter);
+
let filtered = this.usageReport.lines;
if (this.filters.sku) {
filtered = filtered.filter(line => line.sku === this.filters.sku);
@@ -217,7 +302,15 @@ export class UsageReportService {
getUsageFilteredByProduct(product: Product | Product[]): Observable {
const _products = Array.isArray(product) ? product : [product];
return this.getUsageReportFiltered().pipe(
- map(lines => lines.filter(line => _products.some(p => line.product.includes(p)))),
+ map(lines => lines.filter(line => _products.some(p => {
+ const filtered = line.product.includes(p);
+ if (product === 'actions') {
+ if (line.sku === 'actions_storage') {
+ return false; // Exclude storage from actions
+ }
+ }
+ return filtered;
+ }))),
);
}
@@ -257,6 +350,182 @@ export class UsageReportService {
}
return formatted;
}
-}
-const titlecasePipe = new TitleCasePipe();
\ No newline at end of file
+ calculatePercentageChange(oldValue: number, newValue: number): number {
+ return (oldValue === 0) ? 0 : ((newValue - oldValue) / oldValue) * 100;
+ }
+
+ getAggregatedUsageData(aggregationType: AggregationType, product?: Product | Product[]): Observable {
+ const productFilter = product || 'actions';
+
+ return this.getUsageFilteredByProduct(productFilter).pipe(
+ map(data => {
+ // Calculate aggregated data directly without caching
+ return this.aggregateUsageData(data, aggregationType);
+ })
+ );
+ }
+
+ private aggregateUsageData(data: CustomUsageReportLine[], aggregationType: AggregationType): AggregatedUsageData {
+ const availableMonths: string[] = [];
+ const dateRange = { start: new Date(), end: new Date() };
+
+ const usageItems = data.reduce((acc, line) => {
+ const item = acc.find(a => {
+ switch (aggregationType) {
+ case 'sku':
+ return (a as WorkflowUsageItem).sku === this.formatSku(line.sku);
+ case 'organization':
+ return (a as WorkflowUsageItem).organization === line.organization;
+ case 'costCenterName':
+ return (a as WorkflowUsageItem).costCenterName === line.costCenterName;
+ case 'repositoryName':
+ return (a as WorkflowUsageItem).repositoryName === line.repositoryName;
+ case 'workflow':
+ return (a as WorkflowUsageItem).workflowName === line.workflowName;
+ case 'workflowPath':
+ return (a as WorkflowUsageItem).workflowPath === line.workflowPath;
+ case 'username':
+ return (a as WorkflowUsageItem).username === line.username;
+ default:
+ return false;
+ }
+ });
+
+ const month: string = line.date.toLocaleString('default', { month: 'short', year: '2-digit' });
+
+ // Track available months
+ if (!availableMonths.includes(month)) {
+ availableMonths.push(month);
+ }
+
+ // Track date range
+ if (line.date < dateRange.start) dateRange.start = line.date;
+ if (line.date > dateRange.end) dateRange.end = line.date;
+
+ if (item) {
+ if ((item as any)[month]) {
+ (item as any)[month] += line.value;
+ } else {
+ (item as any)[month] = line.value || 0;
+ }
+
+ item.cost += line.quantity * line.pricePerUnit;
+ item.total += line.quantity;
+ item.runs++;
+ } else {
+ const newItem: WorkflowUsageItem = {
+ total: line.quantity,
+ cost: line.quantity * line.pricePerUnit,
+ runs: 1,
+ pricePerUnit: line.pricePerUnit || 0,
+ avgCost: line.quantity * line.pricePerUnit,
+ avgTime: line.value,
+ [month]: line.value,
+ workflowName: line.workflowName,
+ workflowPath: line.workflowPath,
+ costCenterName: line.costCenterName,
+ organization: line.organization,
+ repositoryName: line.repositoryName,
+ sku: this.formatSku(line.sku),
+ username: line.username,
+ // shared-storage
+ avgSize: line.value,
+ costPerDay: line.quantity * line.pricePerUnit,
+ count: 1, // For shared storage
+ };
+
+ // Set aggregation-specific properties
+ switch (aggregationType) {
+ case 'workflow':
+ newItem.workflowName = line.workflowName;
+ newItem.repositoryName = line.repositoryName;
+ newItem.sku = this.formatSku(line.sku);
+ newItem.username = line.username;
+ break;
+ case 'workflowPath':
+ newItem.workflowName = line.workflowName;
+ newItem.workflowPath = line.workflowPath;
+ newItem.costCenterName = line.costCenterName;
+ newItem.organization = line.organization;
+ newItem.sku = this.formatSku(line.sku);
+ newItem.username = line.username;
+ break;
+ case 'costCenterName':
+ newItem.costCenterName = line.costCenterName;
+ newItem.organization = line.organization;
+ newItem.sku = this.formatSku(line.sku);
+ newItem.username = line.username;
+ break;
+ case 'organization':
+ newItem.organization = line.organization;
+ newItem.sku = this.formatSku(line.sku);
+ newItem.username = line.username;
+ break;
+ case 'repositoryName':
+ newItem.repositoryName = line.repositoryName;
+ newItem.sku = this.formatSku(line.sku);
+ break;
+ case 'sku':
+ newItem.sku = this.formatSku(line.sku);
+ break;
+ case 'username':
+ newItem.username = line.username;
+ break;
+ }
+
+ acc.push(newItem);
+ }
+ return acc;
+ }, [] as any[]);
+
+ // Calculate averages and ensure all items have all month properties
+ usageItems.forEach((item) => {
+ availableMonths.forEach((month: string) => {
+ if (!(item as any)[month]) {
+ (item as any)[month] = 0;
+ }
+
+ // Calculate percentage change from previous month if needed
+ const monthIndex = availableMonths.indexOf(month);
+ if (monthIndex > 0) {
+ const previousMonth = availableMonths[monthIndex - 1];
+ const lastMonthValue = (item as any)[previousMonth] || 0;
+ const percentageChanged = this.calculatePercentageChange(lastMonthValue, (item as any)[month]);
+ (item as any)[month + 'PercentChange'] = percentageChanged;
+ }
+ });
+
+ item.avgTime = item.runs > 0 ? item.total / item.runs : 0;
+ item.avgCost = item.runs > 0 ? item.cost / item.runs : 0;
+ });
+
+ // Sort months chronologically
+ availableMonths.sort((a, b) => {
+ const dateA = new Date(a);
+ const dateB = new Date(b);
+ return dateA.getTime() - dateB.getTime();
+ });
+
+ return {
+ items: usageItems,
+ availableMonths: availableMonths,
+ dateRange: dateRange
+ };
+ }
+
+ // Helper method for components to get month columns
+ createMonthColumns(availableMonths: string[], currency: 'minutes' | 'cost', dataSource?: any): UsageColumn[] {
+ return availableMonths.map(month => ({
+ columnDef: month,
+ header: month,
+ cell: (item: any) => item[month] || 0,
+ footer: () => {
+ if (!dataSource?.data) return '';
+ const total = dataSource.data.reduce((acc: number, item: any) => acc + (item[month] || 0), 0);
+ return total;
+ },
+ date: new Date(month)
+ }));
+ }
+}
\ No newline at end of file
diff --git a/src/material.module.ts b/src/material.module.ts
index a796d50..e522b44 100644
--- a/src/material.module.ts
+++ b/src/material.module.ts
@@ -20,6 +20,7 @@ import { MatTabsModule } from '@angular/material/tabs';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatMenuModule } from '@angular/material/menu';
+import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
@NgModule({
exports: [
@@ -28,7 +29,6 @@ import { MatMenuModule } from '@angular/material/menu';
MatTableModule,
MatSortModule,
MatPaginatorModule,
- MatButtonModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
@@ -47,7 +47,8 @@ import { MatMenuModule } from '@angular/material/menu';
MatTabsModule,
MatListModule,
MatTooltipModule,
- MatMenuModule
+ MatMenuModule,
+ MatBottomSheetModule
]
})
export class MaterialModule { }
diff --git a/src/styles.scss b/src/styles.scss
index 519709a..b4e5deb 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -911,6 +911,7 @@ h1 {
.logo {
margin-top: 3rem;
+ width: auto;
}
.hide {
@@ -950,4 +951,8 @@ mat-cell {
mat-icon {
vertical-align: bottom !important;
}
+}
+
+.spacer {
+ flex: 1 1 auto;
}
\ No newline at end of file