diff --git a/package-lock.json b/package-lock.json index 4453296..518aaef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "@angular/platform-browser-dynamic": "15.1.5", "@angular/router": "15.1.5", "chart.js": "^4.3.0", + "chartjs-adapter-moment": "^1.0.1", + "moment": "^2.29.4", "primeflex": "^3.3.0", "primeng": "15.2.0", "rxjs": "~7.8.0", @@ -4797,6 +4799,15 @@ "pnpm": ">=7" } }, + "node_modules/chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "peerDependencies": { + "chart.js": ">=3.0.0", + "moment": "^2.10.2" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -8573,6 +8584,14 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -15527,6 +15546,12 @@ "@kurkle/color": "^0.3.0" } }, + "chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "requires": {} + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -18417,6 +18442,11 @@ "minimist": "^1.2.6" } }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 7edb83d..3def581 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "@angular/platform-browser-dynamic": "15.1.5", "@angular/router": "15.1.5", "chart.js": "^4.3.0", + "chartjs-adapter-moment": "^1.0.1", + "moment": "^2.29.4", "primeflex": "^3.3.0", "primeng": "15.2.0", "rxjs": "~7.8.0", @@ -38,4 +40,4 @@ "karma-jasmine-html-reporter": "~2.0.0", "typescript": "~4.8.2" } -} \ No newline at end of file +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 008360c..a65cca3 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -3,6 +3,7 @@ import { RouterModule, Routes } from '@angular/router'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import { SplashComponent } from './components/splash/splash.component'; +import { WorkerComponent } from './components/worker/worker.component'; import { AppLayoutComponent } from './layout/app.layout.component'; const routes: Routes = [ @@ -16,7 +17,16 @@ const routes: Routes = [ children: [ { path: ':address', - component: DashboardComponent + children: [ + { + path: '', + component: DashboardComponent, + }, + { + path: ':workerId', + component: WorkerComponent + } + ] } ] } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ee2a7b7..91d1b8d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,3 +1,5 @@ +import 'chartjs-adapter-moment'; + import { CommonModule, HashLocationStrategy, LocationStrategy } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -8,13 +10,19 @@ import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import { SplashComponent } from './components/splash/splash.component'; +import { WorkerComponent } from './components/worker/worker.component'; import { AppLayoutModule } from './layout/app.layout.module'; +import { NumberSuffixPipe } from './pipes/number-suffix.pipe'; + + @NgModule({ declarations: [ AppComponent, SplashComponent, - DashboardComponent + DashboardComponent, + WorkerComponent, + NumberSuffixPipe ], imports: [ CommonModule, diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html index 64d1f38..f56b5e6 100644 --- a/src/app/components/dashboard/dashboard.component.html +++ b/src/app/components/dashboard/dashboard.component.html @@ -1,5 +1,32 @@ -
-
-        {{clientInfo$ | async | json}}
-    
-
\ No newline at end of file + +
+ + + + + + Name + ID + Hash Rate + Best Luck + + + + + {{worker.name}} + {{worker.id}} + {{worker.hashRate}} + {{worker.bestDifficulty}} + + + + + + + + + +
+ + +
\ No newline at end of file diff --git a/src/app/components/dashboard/dashboard.component.ts b/src/app/components/dashboard/dashboard.component.ts index d8cad54..69a1216 100644 --- a/src/app/components/dashboard/dashboard.component.ts +++ b/src/app/components/dashboard/dashboard.component.ts @@ -11,9 +11,13 @@ import { ClientService } from '../../services/client.service'; }) export class DashboardComponent { + public clientInfo$: Observable; constructor(private clientService: ClientService, private route: ActivatedRoute) { this.clientInfo$ = this.clientService.getClientInfo(this.route.snapshot.params['address']); + + + } } diff --git a/src/app/components/worker/worker.component.html b/src/app/components/worker/worker.component.html new file mode 100644 index 0000000..e8fc9fa --- /dev/null +++ b/src/app/components/worker/worker.component.html @@ -0,0 +1,75 @@ +
+ +
+
+
+
+ ID +
{{workerInfo.name}}
+
+
+ +
+
+ + {{workerInfo.id}} +
+
+
+
+
+
+ Best Luck +
{{workerInfo.bestDifficulty | numberSuffix}} +
+
+
+ +
+
+ {{workerInfo.bestDifficulty | number}} +
+
+ +
+ + +
+
+ +
+
+
\ No newline at end of file diff --git a/src/app/components/worker/worker.component.scss b/src/app/components/worker/worker.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/worker/worker.component.spec.ts b/src/app/components/worker/worker.component.spec.ts new file mode 100644 index 0000000..28e4b46 --- /dev/null +++ b/src/app/components/worker/worker.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { WorkerComponent } from './worker.component'; + +describe('WorkerComponent', () => { + let component: WorkerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ WorkerComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(WorkerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/worker/worker.component.ts b/src/app/components/worker/worker.component.ts new file mode 100644 index 0000000..3e2a3ce --- /dev/null +++ b/src/app/components/worker/worker.component.ts @@ -0,0 +1,121 @@ +import { Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { map, Observable, shareReplay } from 'rxjs'; + +import { WorkerService } from '../../services/worker.service'; + +@Component({ + selector: 'app-worker', + templateUrl: './worker.component.html', + styleUrls: ['./worker.component.scss'] +}) +export class WorkerComponent { + + public workerInfo$: Observable; + + public chartData$: Observable; + + public chartOptions: any; + + constructor(private workerService: WorkerService, private route: ActivatedRoute) { + const documentStyle = getComputedStyle(document.documentElement); + const textColor = documentStyle.getPropertyValue('--text-color'); + const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary'); + const surfaceBorder = documentStyle.getPropertyValue('--surface-border'); + + this.workerInfo$ = this.workerService.getWorkerInfo(this.route.snapshot.params['address'], this.route.snapshot.params['workerId']).pipe( + shareReplay({ bufferSize: 1, refCount: true }) + ) + + this.chartData$ = this.workerInfo$.pipe( + map((workerInfo: any) => { + + const GROUP_SIZE = 20; + const slice = workerInfo.hashData.length % GROUP_SIZE == 0 ? workerInfo.hashData : workerInfo.hashData.slice(0, workerInfo.hashData.length - workerInfo.hashData.length % GROUP_SIZE) + const reducedData = ((slice as any[]) + .reduce((pre, cur, idx, arr) => { + if (idx % GROUP_SIZE != 0) { + pre[pre.length - 1].difficulty += cur.difficulty; + } else { + pre.push(cur); + } + return pre; + }, []) as any[]); + + const labels = reducedData.reduce((pre, cur, idx, arr) => { + if (idx == 0) { + return pre; + } + pre.push(new Date(cur.time)); + return pre; + }, []); + + const data = reducedData + .reduce((pre, cur, idx, arr) => { + if (idx == 0) { + return pre; + } + //hashrate = (nonce_difficulty * 2^32) / time_to_find + pre.push((cur.difficulty * 4294967296 / (new Date(cur.time).getTime() - new Date(arr[idx - 1].time).getTime())) / 1000000); + return pre; + + }, []); + return { + labels, + datasets: [ + { + label: workerInfo.name, + data, + fill: false, + backgroundColor: documentStyle.getPropertyValue('--bluegray-700'), + borderColor: documentStyle.getPropertyValue('--bluegray-700'), + tension: .4 + } + ] + } + }) + ); + + + + this.chartOptions = { + plugins: { + legend: { + labels: { + color: textColor + } + } + }, + scales: { + x: { + type: 'time', + time: { + unit: 'minute', // Set the unit to 'minute' + stepSize: 10, // Set the desired interval between labels in minutes + + }, + ticks: { + color: textColorSecondary + }, + grid: { + color: surfaceBorder, + drawBorder: false + } + }, + y: { + ticks: { + color: textColorSecondary, + callback: (value: number) => value + 'GH/s', + }, + grid: { + color: surfaceBorder, + drawBorder: false + } + } + } + }; + } + + + +} diff --git a/src/app/layout/app.menu.component.html b/src/app/layout/app.menu.component.html index 2171c9f..f8684e0 100644 --- a/src/app/layout/app.menu.component.html +++ b/src/app/layout/app.menu.component.html @@ -1,5 +1,5 @@
    - +
  • diff --git a/src/app/layout/app.menu.component.ts b/src/app/layout/app.menu.component.ts index 8f17579..95a5cfe 100644 --- a/src/app/layout/app.menu.component.ts +++ b/src/app/layout/app.menu.component.ts @@ -1,4 +1,6 @@ import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Router } from '@angular/router'; +import { filter, map, mergeMap, Observable, startWith } from 'rxjs'; import { LayoutService } from './service/app.layout.service'; @@ -8,19 +10,62 @@ import { LayoutService } from './service/app.layout.service'; }) export class AppMenuComponent implements OnInit { - model: any[] = []; + private params$!: Observable; + public model$!: Observable; - constructor(public layoutService: LayoutService) { } + constructor(public layoutService: LayoutService, private router: Router, private activatedRoute: ActivatedRoute) { } ngOnInit() { - this.model = [ - { - label: 'Home', - items: [ - { label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: ['/'] } - ] - }, - ]; + const rootRoute = this.activatedRoute.root; + const initialParams = this.extractRouteParams(rootRoute.snapshot); + + this.params$ = this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + map(() => this.activatedRoute), + map((route) => { + while (route.firstChild) { + route = route.firstChild; + } + return route; + }), + filter((route) => route.outlet === 'primary'), // Adjust the outlet name if necessary + mergeMap((route) => route.params), + startWith(initialParams) + ); + + this.model$ = this.params$.pipe(map(params => { + + return [ + { + label: 'Home', + items: [ + { label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: [params.address] } + ] + }, + + ]; + + })) + + } + + private extractRouteParams(routeSnapshot: ActivatedRouteSnapshot): any { + let route = routeSnapshot; + let params = {}; + + while (route.firstChild) { + route = route.firstChild; + } + + if (route && route.params) { + params = route.params; + } + + return params; + } + + } diff --git a/src/app/pipes/number-suffix.pipe.spec.ts b/src/app/pipes/number-suffix.pipe.spec.ts new file mode 100644 index 0000000..126503b --- /dev/null +++ b/src/app/pipes/number-suffix.pipe.spec.ts @@ -0,0 +1,8 @@ +import { NumberSuffixPipe } from './number-suffix.pipe'; + +describe('NumberSuffixPipe', () => { + it('create an instance', () => { + const pipe = new NumberSuffixPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src/app/pipes/number-suffix.pipe.ts b/src/app/pipes/number-suffix.pipe.ts new file mode 100644 index 0000000..1265ec6 --- /dev/null +++ b/src/app/pipes/number-suffix.pipe.ts @@ -0,0 +1,24 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'numberSuffix', + pure: true +}) +export class NumberSuffixPipe implements PipeTransform { + + transform(value: number): string { + + const suffixes = ['', 'k', 'M', 'B', 'T', 'P', 'E']; + + if (value === 0) { + return '0'; + } + + const power = Math.floor(Math.log10(value) / 3); + const scaledValue = value / Math.pow(1000, power); + const suffix = suffixes[power]; + + return scaledValue.toFixed(1) + suffix; + } + +} diff --git a/src/app/services/worker.service.spec.ts b/src/app/services/worker.service.spec.ts new file mode 100644 index 0000000..7060c48 --- /dev/null +++ b/src/app/services/worker.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { WorkerService } from './worker.service'; + +describe('WorkerService', () => { + let service: WorkerService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(WorkerService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/worker.service.ts b/src/app/services/worker.service.ts new file mode 100644 index 0000000..f06d7d7 --- /dev/null +++ b/src/app/services/worker.service.ts @@ -0,0 +1,20 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { environment } from '../../environments/environment'; + + +@Injectable({ + providedIn: 'root' +}) +export class WorkerService { + + constructor( + private httpClient: HttpClient + ) { } + + public getWorkerInfo(address: string, workerId: string): Observable { + return this.httpClient.get(`${environment.API_URL}/api/client/${address}/${workerId}`); + } +} diff --git a/src/assets/layout/styles/layout/_main.scss b/src/assets/layout/styles/layout/_main.scss index 61cc1b5..269e52a 100644 --- a/src/assets/layout/styles/layout/_main.scss +++ b/src/assets/layout/styles/layout/_main.scss @@ -3,7 +3,7 @@ } html { - height: 100%; + font-size: $scale; } diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 6f24758..4a34310 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,4 +1,4 @@ export const environment = { production: false, - API_URL: '' + API_URL: 'http://localhost:3334' }; diff --git a/src/styles.scss b/src/styles.scss index 950435b..8d4cd4f 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,4 +1,12 @@ @import "assets/layout/styles/layout/layout.scss"; @import "../node_modules/primeng/resources/primeng.min.css"; @import "../node_modules/primeicons/primeicons.css"; -@import "../node_modules/primeflex/primeflex.css"; \ No newline at end of file +@import "../node_modules/primeflex/primeflex.css"; + +.p-datatable .p-datatable-thead>tr>th { + background: none; +} + +.p-datatable.p-datatable-hoverable-rows .p-datatable-tbody>tr:not(.p-highlight) { + cursor: pointer; +} \ No newline at end of file