diff --git a/src/app/app.component.html b/src/app/app.component.html index 3f7aab3..705e9b7 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,3 +1,15 @@ -

GitHub Usage Report

-

This is an entirely client side application. Your usage report never leaves your computer.

- \ No newline at end of file +
+
+ +

+ Github Usage Report +

+
+
+

Visualize your Github usage

+

This is an entirely client side application. Your usage report never leaves your computer.

+ +
+
\ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 52ff0d9..e0509c9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -7,8 +7,9 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { UsageComponent } from './usage/usage.component'; import { UsageTableComponent } from './usage-table/usage-table.component'; import { FileUploadComponent } from './file-upload/file-upload.component'; -import { ChartPieOwnerComponent } from './chart-pie-owner/chart-pie-owner.component'; +import { ChartPieUserComponent } from './chart-pie-user/chart-pie-user.component'; import { ChartLineUsageTimeComponent } from './chart-line-usage-time/chart-line-usage-time.component'; +import { TableWorkflowUsageComponent } from './table-workflow-usage/table-workflow-usage.component'; import { HighchartsChartModule } from 'highcharts-angular'; @@ -18,8 +19,9 @@ import { HighchartsChartModule } from 'highcharts-angular'; UsageComponent, UsageTableComponent, FileUploadComponent, - ChartPieOwnerComponent, - ChartLineUsageTimeComponent + ChartPieUserComponent, + ChartLineUsageTimeComponent, + TableWorkflowUsageComponent ], imports: [ BrowserModule, diff --git a/src/app/chart-pie-owner/chart-pie-owner.component.html b/src/app/chart-pie-user/chart-pie-user.component.html similarity index 100% rename from src/app/chart-pie-owner/chart-pie-owner.component.html rename to src/app/chart-pie-user/chart-pie-user.component.html diff --git a/src/app/chart-pie-owner/chart-pie-owner.component.scss b/src/app/chart-pie-user/chart-pie-user.component.scss similarity index 100% rename from src/app/chart-pie-owner/chart-pie-owner.component.scss rename to src/app/chart-pie-user/chart-pie-user.component.scss diff --git a/src/app/chart-pie-owner/chart-pie-owner.component.spec.ts b/src/app/chart-pie-user/chart-pie-user.component.spec.ts similarity index 57% rename from src/app/chart-pie-owner/chart-pie-owner.component.spec.ts rename to src/app/chart-pie-user/chart-pie-user.component.spec.ts index 6f25b95..345f9a2 100644 --- a/src/app/chart-pie-owner/chart-pie-owner.component.spec.ts +++ b/src/app/chart-pie-user/chart-pie-user.component.spec.ts @@ -1,18 +1,18 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ChartPieOwnerComponent } from './chart-pie-owner.component'; +import { ChartPieUserComponent } from './chart-pie-user.component'; describe('ChartPieOwnerComponent', () => { - let component: ChartPieOwnerComponent; - let fixture: ComponentFixture; + let component: ChartPieUserComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ ChartPieOwnerComponent ] + declarations: [ ChartPieUserComponent ] }) .compileComponents(); - fixture = TestBed.createComponent(ChartPieOwnerComponent); + fixture = TestBed.createComponent(ChartPieUserComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/chart-pie-owner/chart-pie-owner.component.ts b/src/app/chart-pie-user/chart-pie-user.component.ts similarity index 77% rename from src/app/chart-pie-owner/chart-pie-owner.component.ts rename to src/app/chart-pie-user/chart-pie-user.component.ts index 3c3d668..3c2faff 100644 --- a/src/app/chart-pie-owner/chart-pie-owner.component.ts +++ b/src/app/chart-pie-user/chart-pie-user.component.ts @@ -3,11 +3,11 @@ import { UsageReportLine } from 'github-usage-report/types'; import * as Highcharts from 'highcharts'; @Component({ - selector: 'app-chart-pie-owner', - templateUrl: './chart-pie-owner.component.html', - styleUrls: ['./chart-pie-owner.component.scss'] + selector: 'app-chart-pie-user', + templateUrl: './chart-pie-user.component.html', + styleUrls: ['./chart-pie-user.component.scss'] }) -export class ChartPieOwnerComponent { +export class ChartPieUserComponent { @Input() data!: UsageReportLine[]; Highcharts: typeof Highcharts = Highcharts; options: Highcharts.Options = { @@ -15,7 +15,7 @@ export class ChartPieOwnerComponent { type: 'pie' }, title: { - text: 'Usage by Owner' + text: 'Usage by username' }, tooltip: { pointFormat: '{series.name}: {point.percentage:.1f}%' @@ -38,9 +38,9 @@ export class ChartPieOwnerComponent { type: 'pie', // Add the type property name: 'Usage', data: this.data.reduce((acc, line) => { - const index = acc.findIndex((item) => item[0] === line.owner); + const index = acc.findIndex((item) => item[0] === line.username); if (index === -1) { - acc.push([line.owner, line.quantity]); + acc.push([line.username, line.quantity]); } else { acc[index][1] += line.quantity; } diff --git a/src/app/file-upload/file-upload.component.html b/src/app/file-upload/file-upload.component.html index 5d78da0..b53bd55 100644 --- a/src/app/file-upload/file-upload.component.html +++ b/src/app/file-upload/file-upload.component.html @@ -1,2 +1,5 @@ - - + + \ No newline at end of file diff --git a/src/app/table-workflow-usage/table-workflow-usage.component.html b/src/app/table-workflow-usage/table-workflow-usage.component.html new file mode 100644 index 0000000..a61bf12 --- /dev/null +++ b/src/app/table-workflow-usage/table-workflow-usage.component.html @@ -0,0 +1,21 @@ +

Workflow Usage

+ +
+ + @for (column of columns; track column) { + + + + + } + + + +
+ {{column.header}} + + {{column.cell(row)}} +
+ + +
\ No newline at end of file diff --git a/src/app/table-workflow-usage/table-workflow-usage.component.scss b/src/app/table-workflow-usage/table-workflow-usage.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/table-workflow-usage/table-workflow-usage.component.spec.ts b/src/app/table-workflow-usage/table-workflow-usage.component.spec.ts new file mode 100644 index 0000000..82c26d5 --- /dev/null +++ b/src/app/table-workflow-usage/table-workflow-usage.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TableWorkflowUsageComponent } from './table-workflow-usage.component'; + +describe('TableWorkflowUsageComponent', () => { + let component: TableWorkflowUsageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TableWorkflowUsageComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TableWorkflowUsageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/table-workflow-usage/table-workflow-usage.component.ts b/src/app/table-workflow-usage/table-workflow-usage.component.ts new file mode 100644 index 0000000..eff33a9 --- /dev/null +++ b/src/app/table-workflow-usage/table-workflow-usage.component.ts @@ -0,0 +1,77 @@ +import { Component, Input, ViewChild } from '@angular/core'; +import { UsageReport, UsageReportLine } from 'github-usage-report/types'; +import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatSort } from '@angular/material/sort'; + +@Component({ + selector: 'app-table-workflow-usage', + templateUrl: './table-workflow-usage.component.html', + styleUrl: './table-workflow-usage.component.scss' +}) +export class TableWorkflowUsageComponent { + columns = [ + { + columnDef: 'workflow', + header: 'Workflow', + cell: (workflowItem: any) => `${workflowItem.workflow}`, + }, + { + columnDef: 'total', + header: 'Total', + cell: (workflowItem: any) => `${workflowItem.total}`, + } + ]; + displayedColumns = this.columns.map(c => c.columnDef); + @Input() data!: UsageReport; + dataSource: MatTableDataSource = new MatTableDataSource(); // Initialize the dataSource property + + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort!: MatSort; + + ngOnInit() { + const workflowUsage = this.data.lines.filter(a => a.actionsWorkflow).reduce((acc, line) => { + const workflowEntry = acc.find(a => a.workflow === line.actionsWorkflow); + const date = new Date(line.date); + const month: string = date.toLocaleString('default', { month: 'long' }); + if (workflowEntry) { + if (workflowEntry[month]) { + workflowEntry[month] += line.quantity || 0; + } else { + workflowEntry[month] = line.quantity || 0; + } + workflowEntry.total += line.quantity || 0; + if (!this.columns.find(c => c.columnDef === month)) { + this.columns.push({ + columnDef: month, + header: month, + cell: (cost: any) => `${cost[month]}`, + }); + this.displayedColumns = this.columns.map(c => c.columnDef); + } + } else { + acc.push({ + workflow: line.actionsWorkflow, + repo: line.repositorySlug, + total: line.quantity || 0, + [month]: line.quantity || 0 + }); + } + return acc; // Add this line to return the accumulator + }, [] as any[]); + // fill in undefined months in workflowUsage + workflowUsage.forEach((workflowItem: any) => { + this.columns.forEach((column: any) => { + if (!workflowItem[column.columnDef]) { + workflowItem[column.columnDef] = 0; + } + }); + }); + this.dataSource = new MatTableDataSource(workflowUsage); + } + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } +} diff --git a/src/app/usage-table/usage-table.component.ts b/src/app/usage-table/usage-table.component.ts index 8acb407..d5d7a0d 100644 --- a/src/app/usage-table/usage-table.component.ts +++ b/src/app/usage-table/usage-table.component.ts @@ -2,7 +2,7 @@ import { Input } from '@angular/core'; import {AfterViewInit, Component, ViewChild} from '@angular/core'; import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator'; import {MatTableDataSource, MatTableModule} from '@angular/material/table'; -import { UsageReportLine } from 'github-usage-report/types'; +import { UsageReport, UsageReportLine } from 'github-usage-report/types'; /** * @title Table with pagination diff --git a/src/app/usage/usage.component.html b/src/app/usage/usage.component.html index f5f158c..0e3c003 100644 --- a/src/app/usage/usage.component.html +++ b/src/app/usage/usage.component.html @@ -1,8 +1,9 @@ + + - + + - - diff --git a/src/app/usage/usage.component.ts b/src/app/usage/usage.component.ts index 1bb1490..e6712a3 100644 --- a/src/app/usage/usage.component.ts +++ b/src/app/usage/usage.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { ChangeDetectorRef, Component } from '@angular/core'; import { UsageReport } from 'github-usage-report/types'; import { readGithubUsageReport } from 'github-usage-report/usage-report'; @@ -10,6 +10,8 @@ import { readGithubUsageReport } from 'github-usage-report/usage-report'; export class UsageComponent { usage: UsageReport | null = null; + constructor(private cdr: ChangeDetectorRef) { } // inject ChangeDetectorRef + ngOnInit() { const oldUsage = localStorage.getItem('usage'); this.usage = oldUsage ? JSON.parse(oldUsage) : null; @@ -18,5 +20,6 @@ export class UsageComponent { async onFileText(fileText: string) { const usage = await readGithubUsageReport(fileText); this.usage = usage; + this.cdr.detectChanges(); // manually trigger change detection } } diff --git a/src/assets/github-mark-white.svg b/src/assets/github-mark-white.svg new file mode 100644 index 0000000..d5e6491 --- /dev/null +++ b/src/assets/github-mark-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/github-mark.svg b/src/assets/github-mark.svg new file mode 100644 index 0000000..37fa923 --- /dev/null +++ b/src/assets/github-mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/index.html b/src/index.html index 5050250..f017afc 100644 --- a/src/index.html +++ b/src/index.html @@ -10,7 +10,7 @@ - + diff --git a/src/material.module.ts b/src/material.module.ts index 2b095d6..bf1cf56 100644 --- a/src/material.module.ts +++ b/src/material.module.ts @@ -6,6 +6,7 @@ import { MatTableModule } from '@angular/material/table'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; @NgModule({ exports: [ @@ -17,7 +18,8 @@ import { MatButtonModule } from '@angular/material/button'; MatButtonModule, MatTableModule, MatPaginatorModule, - MatSortModule + MatSortModule, + MatIconModule ] }) export class MaterialModule { } diff --git a/src/material.theme.scss b/src/material.theme.scss new file mode 100644 index 0000000..63a17c0 --- /dev/null +++ b/src/material.theme.scss @@ -0,0 +1,34 @@ +@use '@angular/material' as mat; + +@include mat.core(); + +// Define a dark theme +$dark-theme: mat.define-dark-theme(( + color: ( + primary: mat.define-palette(mat.$pink-palette), + accent: mat.define-palette(mat.$blue-grey-palette), + ), + // Only include `typography` and `density` in the default dark theme. + typography: mat.define-typography-config(), + density: 0, +)); + +// Define a light theme +$light-theme: mat.define-light-theme(( + color: ( + primary: mat.define-palette(mat.$indigo-palette), + accent: mat.define-palette(mat.$pink-palette), + ), +)); + +// Apply the dark theme by default +// @include mat.core-theme($dark-theme); +// @include mat.button-theme($dark-theme); + +// Apply the light theme only when the user prefers light themes. +@media (prefers-color-scheme: light) { + // Use the `-color` mixins to only apply color styles without reapplying the same + // typography and density styles. + @include mat.core-color($light-theme); + @include mat.button-color($light-theme); +} \ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index 6293ddc..b437cc2 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,7 +1,49 @@ -/* You can add global styles to this file, and also import other style files */ +@import './material.theme.scss'; -html, body { height: 100%; } -body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } +html, +body { + height: 100%; +} -html, body { height: 100%; } -body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } +body { + margin: 0; +} + +.flex-center { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.flex-center-horizontally { + display: flex; + justify-content: center; + align-items: center; +} + +.fill { + width: 100%; + height: 100%; +} + +.container { + margin: 0 10px; + padding: 0 10px; +} + +.header { + display: flex; + align-items: center; + vertical-align: middle; + margin: 0 0 56px; + padding: 10px; + align-items: center; + h1 { + margin: 0 !important; + padding: 0; + } + .logo { + margin-right: 10px; + } +} \ No newline at end of file