charting, routing and stats
This commit is contained in:
Generated
+30
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<ng-container *ngIf="clientInfo$ | async as clientInfo">
|
||||
<div class="card">
|
||||
<pre>
|
||||
{{clientInfo$ | async | json}}
|
||||
</pre>
|
||||
|
||||
|
||||
<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,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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { NumberSuffixPipe } from './number-suffix.pipe';
|
||||
|
||||
describe('NumberSuffixPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new NumberSuffixPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
|
||||
font-size: $scale;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const environment = {
|
||||
production: false,
|
||||
API_URL: ''
|
||||
API_URL: 'http://localhost:3334'
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user