Polish mining page UI

Make sure to wait for all mining pools queries before continuing
This commit is contained in:
nymkappa 2022-01-21 21:50:57 +09:00
parent 40e529ece7
commit f8f9108ae1
No known key found for this signature in database
GPG Key ID: E155910B16E8BD04
5 changed files with 99 additions and 50 deletions

View File

@ -265,7 +265,7 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/1y', routes.$getStatisticsByTime.bind(routes, '1y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/2y', routes.$getStatisticsByTime.bind(routes, '2y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y')) .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/3y', routes.$getStatisticsByTime.bind(routes, '3y'))
.get(config.MEMPOOL.API_URL_PREFIX + 'pools', routes.getPools) .get(config.MEMPOOL.API_URL_PREFIX + 'pools', routes.$getPools)
; ;
} }

View File

@ -532,9 +532,12 @@ class Routes {
} }
} }
public async getPools(req: Request, res: Response) { public async $getPools(req: Request, res: Response) {
try { try {
let stats = await miningStats.$getPoolsStats(req.query.interval as string); let stats = await miningStats.$getPoolsStats(req.query.interval as string);
// res.header('Pragma', 'public');
// res.header('Cache-control', 'public');
// res.setHeader('Expires', new Date(Date.now() + 1000 * 300).toUTCString());
res.json(stats); res.json(stats);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);

View File

@ -1,13 +1,12 @@
<div class="container-xl"> <div class="container-xl">
<!-- <app-difficulty [showProgress]=false [showHalving]=true></app-difficulty> -->
<app-difficulty style="display: none;" [showProgress]=false [showHalving]=true></app-difficulty> <div class="hashrate-pie" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div style="height: 500px; margin-top: 30px;" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
<div class="text-center loadingGraphs" *ngIf="isLoading"> <div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="spinner-border text-light"></div> <div class="spinner-border text-light"></div>
</div> </div>
<div class="card-header"> <div class="card-header mb-0 mb-lg-4">
<form [formGroup]="radioGroupForm" class="formRadioGroup"> <form [formGroup]="radioGroupForm" class="formRadioGroup">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan"> <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="dateSpan">
<label ngbButtonLabel class="btn-primary btn-sm"> <label ngbButtonLabel class="btn-primary btn-sm">
@ -44,30 +43,32 @@
</form> </form>
</div> </div>
<table class="table table-borderless" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50"> <table class="table table-borderless text-center pools-table" [alwaysCallback]="true" infiniteScroll [infiniteScrollDistance]="1.5" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50">
<thead> <thead>
<th class="d-none d-md-block" i18n="latest-blocks.height">Rank</th> <tr>
<th><!-- LOGO --></th> <th class="d-none d-md-block" i18n="latest-blocks.height">Rank</th>
<th i18n="latest-blocks.timestamp">Name</th> <th class=""></th>
<th *ngIf="this.poolsWindowPreference === '1d'" i18n="latest-blocks.timestamp">Hashrate</th> <th class="" i18n="latest-blocks.poolName">Name</th>
<th i18n="latest-blocks.mined">Block Count (%)</th> <th class="" *ngIf="this.poolsWindowPreference === '1d'" i18n="latest-blocks.timestamp">Hashrate</th>
<th class="d-none d-md-block" i18n="latest-blocks.transactions">Empty Blocks (%)</th> <th class="" i18n="latest-blocks.mined">Blocks</th>
<th class="d-none d-md-block" i18n="latest-blocks.transactions">Empty Blocks</th>
</tr>
</thead> </thead>
<tbody *ngIf="(miningStatsObservable$ | async) as miningStats"> <tbody *ngIf="(miningStatsObservable$ | async) as miningStats">
<tr> <tr>
<td class="d-none d-md-block">-</td> <td class="d-none d-md-block">-</td>
<td><!-- LOGO --></td> <td class="text-right"><img width="25" height="25" src="./resources/mining-pools/default.svg"></td>
<td>All miners</td> <td class="">All miners</td>
<td *ngIf="this.poolsWindowPreference === '1d'">{{ miningStats.lastEstimatedHashrate}} {{ miningStats.miningUnits.hashrateUnit }}</td> <td class="" *ngIf="this.poolsWindowPreference === '1d'">{{ miningStats.lastEstimatedHashrate}} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td>{{ miningStats.blockCount }}</td> <td class="">{{ miningStats.blockCount }}</td>
<td class="d-none d-md-block">{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio }}%)</td> <td class="d-none d-md-block">{{ miningStats.totalEmptyBlock }} ({{ miningStats.totalEmptyBlockRatio }}%)</td>
</tr> </tr>
<tr *ngFor="let pool of miningStats.pools"> <tr *ngFor="let pool of miningStats.pools">
<td class="d-none d-md-block">{{ pool.rank }}</td> <td class="d-none d-md-block">{{ pool.rank }}</td>
<td><img width="25" height="25" src="{{ pool.logo }}" onError="this.src = './resources/mining-pools/default.svg'"></td> <td class="text-right"><img width="25" height="25" src="{{ pool.logo }}" onError="this.src = './resources/mining-pools/default.svg'"></td>
<td><a target="#" href="{{ pool.link }}">{{ pool.name }}</a></td> <td class=""><a target="#" href="{{ pool.link }}">{{ pool.name }}</a></td>
<td *ngIf="this.poolsWindowPreference === '1d'">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td> <td class="" *ngIf="this.poolsWindowPreference === '1d'">{{ pool.lastEstimatedHashrate }} {{ miningStats.miningUnits.hashrateUnit }}</td>
<td>{{ pool.blockCount }} ({{ pool.share }}%)</td> <td class="">{{ pool['blockText'] }}</td>
<td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td> <td class="d-none d-md-block">{{ pool.emptyBlocks }} ({{ pool.emptyBlockRatio }}%)</td>
</tr> </tr>
</tbody> </tbody>

View File

@ -0,0 +1,32 @@
.hashrate-pie {
height: 100%;
min-height: 400px;
@media (max-width: 767.98px) {
min-height: 300px;
}
}
.formRadioGroup {
margin-top: 6px;
display: flex;
flex-direction: column;
@media (min-width: 830px) {
margin-left: 2%;
flex-direction: row;
float: left;
margin-top: 0px;
}
.btn-sm {
font-size: 9px;
@media (min-width: 830px) {
font-size: 14px;
}
}
}
@media (max-width: 767.98px) {
.pools-table th,
.pools-table td {
padding: .3em !important;
}
}

View File

@ -2,7 +2,8 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms'; import { FormBuilder, FormGroup } from '@angular/forms';
import { EChartsOption } from 'echarts'; import { EChartsOption } from 'echarts';
import { combineLatest, Observable, of } from 'rxjs'; import { combineLatest, Observable, of } from 'rxjs';
import { catchError, skip, startWith, switchMap, tap } from 'rxjs/operators'; import { catchError, map, skip, startWith, switchMap, tap } from 'rxjs/operators';
import { SinglePoolStats } from 'src/app/interfaces/node-api.interface';
import { StorageService } from '../..//services/storage.service'; import { StorageService } from '../..//services/storage.service';
import { MiningService, MiningStats } from '../../services/mining.service'; import { MiningService, MiningStats } from '../../services/mining.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
@ -10,11 +11,12 @@ import { StateService } from '../../services/state.service';
@Component({ @Component({
selector: 'app-pool-ranking', selector: 'app-pool-ranking',
templateUrl: './pool-ranking.component.html', templateUrl: './pool-ranking.component.html',
styleUrls: ['./pool-ranking.component.scss'],
styles: [` styles: [`
.loadingGraphs { .loadingGraphs {
position: absolute; position: absolute;
top: 50%; top: 38%;
left: calc(50% - 16px); left: calc(50% - 15px);
z-index: 100; z-index: 100;
} }
`], `],
@ -70,6 +72,10 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
catchError((e) => of(this.getEmptyMiningStat())) catchError((e) => of(this.getEmptyMiningStat()))
); );
}), }),
map(data => {
data.pools = data.pools.map((pool: SinglePoolStats) => this.formatPoolUI(pool));
return data;
}),
tap(data => { tap(data => {
this.isLoading = false; this.isLoading = false;
this.prepareChartOptions(data); this.prepareChartOptions(data);
@ -80,8 +86,17 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
} }
formatPoolUI(pool: SinglePoolStats) {
pool['blockText'] = pool.blockCount.toString() + ` (${pool.share}%)`;
return pool;
}
isMobile() {
return (window.innerWidth <= 767.98);
}
generatePoolsChartSerieData(miningStats) { generatePoolsChartSerieData(miningStats) {
const poolShareThreshold = 0.5; // Do not draw pools which hashrate share is lower than that const poolShareThreshold = this.isMobile() ? 1 : 0.5; // Do not draw pools which hashrate share is lower than that
const data: object[] = []; const data: object[] = [];
miningStats.pools.forEach((pool) => { miningStats.pools.forEach((pool) => {
@ -90,16 +105,23 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
} }
data.push({ data.push({
value: pool.share, value: pool.share,
name: pool.name + ` (${pool.share}%)`, name: pool.name + (this.isMobile() ? `` : ` (${pool.share}%)`),
label: { color: '#FFFFFF' }, label: {
color: '#FFFFFF',
overflow: 'break',
},
tooltip: { tooltip: {
backgroundColor: "#282d47",
textStyle: {
color: "#FFFFFF",
},
formatter: () => { formatter: () => {
if (this.poolsWindowPreference === '1d') { if (this.poolsWindowPreference === '1d') {
return `<u><b>${pool.name}</b></u><br>` + return `<u><b>${pool.name} (${pool.share}%)</b></u><br>` +
pool.lastEstimatedHashrate.toString() + ' PH/s (' + pool.share + `%) pool.lastEstimatedHashrate.toString() + ' PH/s' +
<br>(` + pool.blockCount.toString() + ` blocks)`; `<br>` + pool.blockCount.toString() + ` blocks`;
} else { } else {
return `<u><b>${pool.name}</b></u><br>` + return `<u><b>${pool.name} (${pool.share}%)</b></u><br>` +
pool.blockCount.toString() + ` blocks`; pool.blockCount.toString() + ` blocks`;
} }
} }
@ -116,29 +138,22 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
subtext: (this.poolsWindowPreference === '1d') ? 'Estimated from the # of blocks mined' : null, subtext: (this.poolsWindowPreference === '1d') ? 'Estimated from the # of blocks mined' : null,
left: 'center', left: 'center',
textStyle: { textStyle: {
color: '#FFFFFF', color: '#FFF',
}, },
subtextStyle: { subtextStyle: {
color: '#CCCCCC', color: '#CCC',
fontStyle: 'italic', fontStyle: 'italic',
} }
}, },
tooltip: { tooltip: {
trigger: 'item' trigger: 'item'
}, },
legend: (window.innerWidth <= 767.98) ? {
bottom: '0%',
left: 'center',
textStyle: {
color: '#FFF'
}
} : null,
series: [ series: [
{ {
top: '5%', top: this.isMobile() ? '5%' : '20%',
name: 'Mining pool', name: 'Mining pool',
type: 'pie', type: 'pie',
radius: ['30%', '70%'], radius: this.isMobile() ? ['10%', '50%'] : ['20%', '80%'],
data: this.generatePoolsChartSerieData(miningStats), data: this.generatePoolsChartSerieData(miningStats),
labelLine: { labelLine: {
lineStyle: { lineStyle: {
@ -146,22 +161,20 @@ export class PoolRankingComponent implements OnInit, OnDestroy {
}, },
}, },
label: { label: {
show: (window.innerWidth > 767.98),
fontSize: 14, fontSize: 14,
}, },
itemStyle: { itemStyle: {
borderRadius: 5, borderRadius: 2,
borderWidth: 2, borderWidth: 2,
borderColor: '#000', borderColor: '#000',
}, },
emphasis: { emphasis: {
itemStyle: { itemStyle: {
borderWidth: 5, borderWidth: 2,
borderColor: '#000', borderColor: '#FFF',
borderRadius: 20, borderRadius: 2,
shadowBlur: 40, shadowBlur: 80,
shadowOffsetX: 0, shadowColor: 'rgba(255, 255, 255, 0.75)',
shadowColor: 'rgba(0, 0, 0, 0.75)'
}, },
labelLine: { labelLine: {
lineStyle: { lineStyle: {