Merge pull request #4715 from mempool/nymkappa/accel-dashboard-cleanup

[accelerator] improve acceleration list/dashboard
This commit is contained in:
softsimon 2024-03-01 10:17:22 +07:00 committed by GitHub
commit af6af2748c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 117 additions and 100 deletions

View File

@ -3,16 +3,16 @@
<div class="item"> <div class="item">
<h5 class="card-title" i18n="accelerator.requests">Requests</h5> <h5 class="card-title" i18n="accelerator.requests">Requests</h5>
<div class="card-text"> <div class="card-text">
<div>{{ stats.count }}</div> <div>{{ stats.totalRequested }}</div>
<div class="symbol" i18n="accelerator.total-accelerated">accelerated</div> <div class="symbol" i18n="accelerator.total-accelerated">accelerated</div>
</div> </div>
</div> </div>
<div class="item"> <div class="item">
<h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5> <h5 class="card-title" i18n="accelerator.total-boost">Total Bid Boost</h5>
<div class="card-text"> <div class="card-text">
<div>{{ stats.totalFeesPaid / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div> <div>{{ stats.totalBidBoost / 100_000_000 | amountShortener: 4 }} <span class="symbol" i18n="shared.btc|BTC">BTC</span></div>
<span class="fiat"> <span class="fiat">
<app-fiat [value]="stats.totalFeesPaid"></app-fiat> <app-fiat [value]="stats.totalBidBoost"></app-fiat>
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,9 +1,12 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs'; import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators'; import { ServicesApiServices } from '../../../services/services-api.service';
import { ApiService } from '../../../services/api.service';
import { StateService } from '../../../services/state.service'; export type AccelerationStats = {
import { Acceleration } from '../../../interfaces/node-api.interface'; totalRequested: number;
totalBidBoost: number;
successRate: number;
}
@Component({ @Component({
selector: 'app-acceleration-stats', selector: 'app-acceleration-stats',
@ -12,35 +15,13 @@ import { Acceleration } from '../../../interfaces/node-api.interface';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AccelerationStatsComponent implements OnInit { export class AccelerationStatsComponent implements OnInit {
@Input() timespan: '24h' | '1w' | '1m' = '24h'; accelerationStats$: Observable<AccelerationStats>;
@Input() accelerations$: Observable<Acceleration[]>;
public accelerationStats$: Observable<any>;
constructor( constructor(
private apiService: ApiService, private servicesApiService: ServicesApiServices
private stateService: StateService,
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.accelerationStats$ = this.accelerations$.pipe( this.accelerationStats$ = this.servicesApiService.getAccelerationStats$();
switchMap(accelerations => {
let totalFeesPaid = 0;
let totalSucceeded = 0;
let totalCanceled = 0;
for (const acc of accelerations) {
if (acc.status === 'completed') {
totalSucceeded++;
totalFeesPaid += (acc.feePaid - acc.baseFee - acc.vsizeFee) || 0;
} else if (acc.status === 'failed') {
totalCanceled++;
}
}
return of({
count: totalSucceeded,
totalFeesPaid,
successRate: (totalSucceeded + totalCanceled > 0) ? ((totalSucceeded / (totalSucceeded + totalCanceled)) * 100) : 0.0,
});
})
);
} }
} }

View File

@ -1,4 +1,4 @@
<div class="container-xl widget-container" [class.widget]="widget" [class.full-height]="!widget"> <div class="container-lg widget-container" [class.widget]="widget" [class.full-height]="!widget">
<h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1> <h1 *ngIf="!widget" class="float-left" i18n="master-page.blocks">Accelerations</h1>
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div> <div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
@ -17,6 +17,7 @@
<th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th> <th class="fee text-right" i18n="transaction.bid-boost|Bid Boost">Bid Boost</th>
<th class="block text-right" i18n="accelerator.block">Block</th> <th class="block text-right" i18n="accelerator.block">Block</th>
<th class="status text-right" i18n="transaction.status|Transaction Status">Status</th> <th class="status text-right" i18n="transaction.status|Transaction Status">Status</th>
<th class="date text-right" i18n="" *ngIf="!this.widget">Requested</th>
</ng-container> </ng-container>
</thead> </thead>
<tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''"> <tbody *ngIf="accelerations; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
@ -49,9 +50,13 @@
</td> </td>
<td class="status text-right"> <td class="status text-right">
<span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span> <span *ngIf="acceleration.status === 'accelerating'" class="badge badge-warning" i18n="accelerator.pending">Pending</span>
<span *ngIf="acceleration.status === 'mined' || acceleration.status === 'completed'" class="badge badge-success" i18n="transaction.rbf.mined">Mined</span> <span *ngIf="acceleration.status === 'mined'" class="badge badge-info" i18n="transaction.rbf.mined">Mined</span>
<span *ngIf="acceleration.status === 'completed'" class="badge badge-success" i18n="">Completed</span>
<span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span> <span *ngIf="acceleration.status === 'failed'" class="badge badge-danger" i18n="accelerator.canceled">Canceled</span>
</td> </td>
<td class="date text-right" *ngIf="!this.widget">
<app-time kind="since" [time]="acceleration.added" [fastRender]="true"></app-time>
</td>
</ng-container> </ng-container>
</tr> </tr>
</tbody> </tbody>
@ -75,6 +80,11 @@
</ng-template> </ng-template>
</table> </table>
<ngb-pagination *ngIf="!widget" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
[collectionSize]="this.accelerationCount" [rotate]="true" [maxSize]="maxSize" [pageSize]="15" [(page)]="page"
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
</ngb-pagination>
<ng-template [ngIf]="!widget"> <ng-template [ngIf]="!widget">
<div class="clearfix"></div> <div class="clearfix"></div>
<br> <br>

View File

@ -63,16 +63,28 @@ tr, td, th {
} }
.txid { .txid {
@media (max-width: 500px) { width: 20%;
}
.fee {
width: 15%;
}
.block {
width: 15%;
@media (max-width: 700px) {
display: none; display: none;
} }
} }
.fee, .block, .status { .status {
width: 15%; width: 13%;
}
@media (max-width: 720px) { .date {
width: 20%; width: 20%;
@media (max-width: 600px) {
display: none;
} }
} }
@ -83,23 +95,12 @@ tr, td, th {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 30%; max-width: 30%;
@media (max-width: 1060px) and (min-width: 768px) {
display: none;
}
@media (max-width: 500px) {
display: none;
}
} }
.fee-rate { .fee-rate {
width: 20%; width: 20%;
@media (max-width: 1060px) and (min-width: 768px) { text-align: end !important;
text-align: start !important; @media (max-width: 975px) and (min-width: 768px) {
}
@media (max-width: 500px) {
text-align: start !important;
}
@media (max-width: 840px) and (min-width: 768px) {
display: none; display: none;
} }
@media (max-width: 410px) { @media (max-width: 410px) {
@ -108,32 +109,31 @@ tr, td, th {
} }
.bid { .bid {
text-align: end !important;
width: 30%; width: 30%;
min-width: 150px; min-width: 150px;
@media (max-width: 840px) and (min-width: 768px) {
text-align: start !important;
}
@media (max-width: 410px) {
text-align: start !important;
}
} }
.time { .time {
width: 25%; width: 25%;
@media (max-width: 600px) {
display: none;
}
@media (max-width: 1200px) and (min-width: 768px) {
display: none;
}
} }
.fee { .fee {
width: 30%; width: 30%;
@media (max-width: 1060px) and (min-width: 768px) { text-align: end !important;
text-align: start !important;
}
@media (max-width: 500px) {
text-align: start !important;
}
} }
.block { .block {
width: 20%; width: 20%;
@media (max-width: 1200px) and (min-width: 768px) {
display: none;
}
} }
.status { .status {

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
import { Observable, catchError, of, switchMap, tap } from 'rxjs'; import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs';
import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { WebsocketService } from '../../../services/websocket.service'; import { WebsocketService } from '../../../services/websocket.service';
@ -21,9 +21,10 @@ export class AccelerationsListComponent implements OnInit {
isLoading = true; isLoading = true;
paginationMaxSize: number; paginationMaxSize: number;
page = 1; page = 1;
lastPage = 1; accelerationCount: number;
maxSize = window.innerWidth <= 767.98 ? 3 : 5; maxSize = window.innerWidth <= 767.98 ? 3 : 5;
skeletonLines: number[] = []; skeletonLines: number[] = [];
pageSubject: BehaviorSubject<number> = new BehaviorSubject(this.page);
constructor( constructor(
private servicesApiService: ServicesApiServices, private servicesApiService: ServicesApiServices,
@ -40,34 +41,47 @@ export class AccelerationsListComponent implements OnInit {
this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()]; this.skeletonLines = this.widget === true ? [...Array(6).keys()] : [...Array(15).keys()];
this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5; this.paginationMaxSize = window.matchMedia('(max-width: 670px)').matches ? 3 : 5;
const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistory$({ timeframe: '1m' })); this.accelerationList$ = this.pageSubject.pipe(
this.accelerationList$ = accelerationObservable$.pipe( switchMap((page) => {
switchMap(accelerations => { const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ timeframe: '1m', page: page }));
if (this.pending) { return accelerationObservable$.pipe(
for (const acceleration of accelerations) { switchMap(response => {
acceleration.status = acceleration.status || 'accelerating'; let accelerations = response;
} if (response.body) {
} accelerations = response.body;
for (const acc of accelerations) { this.accelerationCount = parseInt(response.headers.get('x-total-count'), 10);
acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee; }
} if (this.pending) {
if (this.widget) { for (const acceleration of accelerations) {
return of(accelerations.slice(-6).reverse()); acceleration.status = acceleration.status || 'accelerating';
} else { }
return of(accelerations.reverse()); }
} for (const acc of accelerations) {
}), acc.boost = acc.feePaid - acc.baseFee - acc.vsizeFee;
catchError((err) => { }
this.isLoading = false; if (this.widget) {
return of([]); return of(accelerations.slice(0, 6));
}), } else {
tap(() => { return of(accelerations);
this.isLoading = false; }
}),
catchError((err) => {
this.isLoading = false;
return of([]);
}),
tap(() => {
this.isLoading = false;
})
);
}) })
); );
} }
pageChange(page: number): void {
this.pageSubject.next(page);
}
trackByBlock(index: number, block: BlockExtended): number { trackByBlock(index: number, block: BlockExtended): number {
return block.height; return block.height;
} }

View File

@ -27,7 +27,7 @@
<div class="card-wrapper"> <div class="card-wrapper">
<div class="card"> <div class="card">
<div class="card-body more-padding"> <div class="card-body more-padding">
<app-acceleration-stats timespan="1m" [accelerations$]="minedAccelerations$"></app-acceleration-stats> <app-acceleration-stats timespan="1m"></app-acceleration-stats>
</div> </div>
</div> </div>
</div> </div>
@ -84,7 +84,7 @@
<div class="title-link"> <div class="title-link">
<h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5> <h5 class="card-title d-inline" i18n="accelerator.pending-accelerations">Active Accelerations</h5>
</div> </div>
<app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]="true" [accelerations$]="pendingAccelerations$"></app-accelerations-list> <app-accelerations-list [attr.data-cy]="'pending-accelerations'" [widget]=true [pending]=true [accelerations$]="pendingAccelerations$"></app-accelerations-list>
</div> </div>
</div> </div>
</div> </div>

View File

@ -60,7 +60,7 @@ export class AcceleratorDashboardComponent implements OnInit {
this.accelerations$ = this.stateService.chainTip$.pipe( this.accelerations$ = this.stateService.chainTip$.pipe(
distinctUntilChanged(), distinctUntilChanged(),
switchMap(() => { switchMap(() => {
return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m' }).pipe( return this.serviceApiServices.getAccelerationHistory$({ timeframe: '1m', page: 1, pageLength: 100}).pipe(
catchError(() => { catchError(() => {
return of([]); return of([]);
}), }),
@ -71,7 +71,7 @@ export class AcceleratorDashboardComponent implements OnInit {
this.minedAccelerations$ = this.accelerations$.pipe( this.minedAccelerations$ = this.accelerations$.pipe(
map(accelerations => { map(accelerations => {
return accelerations.filter(acc => ['mined', 'completed', 'failed'].includes(acc.status)); return accelerations.filter(acc => ['mined', 'completed'].includes(acc.status));
}) })
); );

View File

@ -393,8 +393,11 @@ export interface Acceleration {
} }
export interface AccelerationHistoryParams { export interface AccelerationHistoryParams {
timeframe?: string, status?: string;
status?: string, timeframe?: string;
pool?: string, poolUniqueId?: number;
blockHash?: string, blockHash?: string;
blockHeight?: number;
page?: number;
pageLength?: number;
} }

View File

@ -7,6 +7,7 @@ import { MenuGroup } from '../interfaces/services.interface';
import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs'; import { Observable, of, ReplaySubject, tap, catchError, share, filter, switchMap } from 'rxjs';
import { IBackendInfo } from '../interfaces/websocket.interface'; import { IBackendInfo } from '../interfaces/websocket.interface';
import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface'; import { Acceleration, AccelerationHistoryParams } from '../interfaces/node-api.interface';
import { AccelerationStats } from '../components/acceleration/acceleration-stats/acceleration-stats.component';
export type ProductType = 'enterprise' | 'community' | 'mining_pool' | 'custom'; export type ProductType = 'enterprise' | 'community' | 'mining_pool' | 'custom';
export interface IUser { export interface IUser {
@ -147,4 +148,12 @@ export class ServicesApiServices {
getAccelerationHistory$(params: AccelerationHistoryParams): Observable<Acceleration[]> { getAccelerationHistory$(params: AccelerationHistoryParams): Observable<Acceleration[]> {
return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } }); return this.httpClient.get<Acceleration[]>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params } });
} }
getAccelerationHistoryObserveResponse$(params: AccelerationHistoryParams): Observable<any> {
return this.httpClient.get<any>(`${SERVICES_API_PREFIX}/accelerator/accelerations/history`, { params: { ...params }, observe: 'response'});
}
getAccelerationStats$(): Observable<AccelerationStats> {
return this.httpClient.get<AccelerationStats>(`${SERVICES_API_PREFIX}/accelerator/accelerations/stats`);
}
} }