charting, routing and stats

This commit is contained in:
Ben Wilson
2023-06-22 09:06:39 -04:00
parent f547adb550
commit b9337ae636
19 changed files with 443 additions and 22 deletions
+30
View File
@@ -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",
+2
View File
@@ -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",
+11 -1
View File
@@ -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
}
]
}
]
}
+9 -1
View File
@@ -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,
@@ -1,5 +1,32 @@
<div class="card">
<pre>
{{clientInfo$ | async | json}}
</pre>
</div>
<ng-container *ngIf="clientInfo$ | async as clientInfo">
<div class="card">
<p-table [rowHover]="true" [value]="clientInfo.workers" [tableStyle]="{ 'min-width': '50rem' }">
<ng-template pTemplate="header">
<tr>
<th>Name</th>
<th>ID</th>
<th>Hash Rate</th>
<th>Best Luck</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-worker>
<tr [routerLink]="[worker.id]">
<td>{{worker.name}}</td>
<td>{{worker.id}}</td>
<td>{{worker.hashRate}}</td>
<td>{{worker.bestDifficulty}}</td>
</tr>
</ng-template>
</p-table>
</div>
</ng-container>
@@ -11,9 +11,13 @@ import { ClientService } from '../../services/client.service';
})
export class DashboardComponent {
public clientInfo$: Observable<any>;
constructor(private clientService: ClientService, private route: ActivatedRoute) {
this.clientInfo$ = this.clientService.getClientInfo(this.route.snapshot.params['address']);
}
}
@@ -0,0 +1,75 @@
<div class="grid">
<ng-container *ngIf="workerInfo$ | async as workerInfo">
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">ID</span>
<div class="text-900 font-medium text-xl">{{workerInfo.name}}</div>
</div>
<div class="flex align-items-center justify-content-center bg-blue-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-id-card text-blue-500 text-xl"></i>
</div>
</div>
<!-- <span class="text-green-500 font-medium">24 new </span> -->
<span class="text-500">{{workerInfo.id}}</span>
</div>
</div>
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Best Luck</span>
<div class="text-900 font-medium text-xl">{{workerInfo.bestDifficulty | numberSuffix}}
</div>
</div>
<div class="flex align-items-center justify-content-center bg-orange-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-star text-orange-500 text-xl"></i>
</div>
</div>
<span class="text-green-500 font-medium">{{workerInfo.bestDifficulty | number}}</span>
</div>
</div>
<!-- <div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Customers</span>
<div class="text-900 font-medium text-xl">28441</div>
</div>
<div class="flex align-items-center justify-content-center bg-cyan-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-inbox text-cyan-500 text-xl"></i>
</div>
</div>
<span class="text-green-500 font-medium">520 </span>
<span class="text-500">newly registered</span>
</div>
</div>
<div class="col-12 lg:col-6 xl:col-3">
<div class="card mb-0">
<div class="flex justify-content-between mb-3">
<div>
<span class="block text-500 font-medium mb-3">Comments</span>
<div class="text-900 font-medium text-xl">152 Unread</div>
</div>
<div class="flex align-items-center justify-content-center bg-purple-100 border-round"
[ngStyle]="{width: '2.5rem', height: '2.5rem'}">
<i class="pi pi-comment text-purple-500 text-xl"></i>
</div>
</div>
<span class="text-green-500 font-medium">85 </span>
<span class="text-500">responded</span>
</div>
</div> -->
</ng-container>
<div class="col-12" *ngIf="chartData$ | async as chartData">
<div class="card">
<p-chart type="line" [data]="chartData" [options]="chartOptions"></p-chart>
</div>
</div>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkerComponent } from './worker.component';
describe('WorkerComponent', () => {
let component: WorkerComponent;
let fixture: ComponentFixture<WorkerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ WorkerComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(WorkerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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<any>;
public chartData$: Observable<any>;
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
}
}
}
};
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
<ul class="layout-menu">
<ng-container *ngFor="let item of model; let i = index;">
<ng-container *ngFor="let item of model$ | async; let i = index;">
<li app-menuitem *ngIf="!item.separator" [item]="item" [index]="i" [root]="true"></li>
<li *ngIf="item.separator" class="menu-separator"></li>
</ng-container>
+49 -4
View File
@@ -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<any>;
public model$!: Observable<any[]>;
constructor(public layoutService: LayoutService) { }
constructor(public layoutService: LayoutService, private router: Router, private activatedRoute: ActivatedRoute) { }
ngOnInit() {
this.model = [
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: ['/'] }
{ 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;
}
}
+8
View File
@@ -0,0 +1,8 @@
import { NumberSuffixPipe } from './number-suffix.pipe';
describe('NumberSuffixPipe', () => {
it('create an instance', () => {
const pipe = new NumberSuffixPipe();
expect(pipe).toBeTruthy();
});
});
+24
View File
@@ -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;
}
}
+16
View File
@@ -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();
});
});
+20
View File
@@ -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<any> {
return this.httpClient.get(`${environment.API_URL}/api/client/${address}/${workerId}`);
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
}
html {
height: 100%;
font-size: $scale;
}
+1 -1
View File
@@ -1,4 +1,4 @@
export const environment = {
production: false,
API_URL: ''
API_URL: 'http://localhost:3334'
};
+8
View File
@@ -2,3 +2,11 @@
@import "../node_modules/primeng/resources/primeng.min.css";
@import "../node_modules/primeicons/primeicons.css";
@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;
}