mirror of
https://github.com/mempool/mempool.git
synced 2025-02-25 07:07:36 +01:00
multi-pool active accelerating details component
This commit is contained in:
parent
46b5b26347
commit
05b022dec8
11 changed files with 244 additions and 7 deletions
|
@ -0,0 +1,35 @@
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" i18n="transaction.accelerated-to-feerate|Accelerated to feerate">Accelerated to</td>
|
||||||
|
<td>
|
||||||
|
<div class="effective-fee-container">
|
||||||
|
@if (accelerationInfo?.acceleratedFeeRate && (!tx.effectiveFeePerVsize || accelerationInfo.acceleratedFeeRate >= tx.effectiveFeePerVsize)) {
|
||||||
|
<app-fee-rate [fee]="accelerationInfo.acceleratedFeeRate"></app-fee-rate>
|
||||||
|
} @else {
|
||||||
|
<app-fee-rate [fee]="tx.effectiveFeePerVsize"></app-fee-rate>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td rowspan="2" *ngIf="tx && (tx.acceleratedBy || accelerationInfo) && miningStats" class="text-right" style="width: 100%;">
|
||||||
|
<div class="chart-container" style="width: 100px; margin-left:auto;">
|
||||||
|
<div
|
||||||
|
echarts
|
||||||
|
*browserOnly
|
||||||
|
class="chart"
|
||||||
|
[initOpts]="chartInitOptions"
|
||||||
|
[options]="chartOptions"
|
||||||
|
style="height: 72px"
|
||||||
|
(chartInit)="onChartInit($event)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="td-width" i18n="transaction.accelerated-by-hashrate|Accelerated to hashrate">Accelerated by</td>
|
||||||
|
<td *ngIf="acceleratedByPercentage">
|
||||||
|
{{ acceleratedByPercentage }} <span class="symbol">of hashrate</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
|
@ -0,0 +1,9 @@
|
||||||
|
.td-width {
|
||||||
|
width: 150px;
|
||||||
|
min-width: 150px;
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
width: 175px;
|
||||||
|
min-width: 175px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { Component, ChangeDetectionStrategy, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
|
import { Transaction } from '../../../interfaces/electrs.interface';
|
||||||
|
import { Acceleration, SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||||
|
import { EChartsOption, PieSeriesOption } from '../../../graphs/echarts';
|
||||||
|
import { MiningStats } from '../../../services/mining.service';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-active-acceleration-box',
|
||||||
|
templateUrl: './active-acceleration-box.component.html',
|
||||||
|
styleUrls: ['./active-acceleration-box.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ActiveAccelerationBox implements OnChanges {
|
||||||
|
@Input() tx: Transaction;
|
||||||
|
@Input() accelerationInfo: Acceleration;
|
||||||
|
@Input() miningStats: MiningStats;
|
||||||
|
|
||||||
|
acceleratedByPercentage: string = '';
|
||||||
|
|
||||||
|
chartOptions: EChartsOption = {};
|
||||||
|
chartInitOptions = {
|
||||||
|
renderer: 'svg',
|
||||||
|
};
|
||||||
|
timespan = '';
|
||||||
|
chartInstance: any = undefined;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnChanges(changes: SimpleChanges): void {
|
||||||
|
if (this.tx && (this.tx.acceleratedBy || this.accelerationInfo) && this.miningStats) {
|
||||||
|
this.prepareChartOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getChartData() {
|
||||||
|
const data: object[] = [];
|
||||||
|
const pools: { [id: number]: SinglePoolStats } = {};
|
||||||
|
for (const pool of this.miningStats.pools) {
|
||||||
|
pools[pool.poolUniqueId] = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDataItem = (value, color, tooltip) => ({
|
||||||
|
value,
|
||||||
|
itemStyle: {
|
||||||
|
color,
|
||||||
|
borderColor: 'rgba(0,0,0,0)',
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
show: true,
|
||||||
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||||
|
borderRadius: 4,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
|
textStyle: {
|
||||||
|
color: 'var(--tooltip-grey)',
|
||||||
|
},
|
||||||
|
borderColor: '#000',
|
||||||
|
formatter: () => {
|
||||||
|
return tooltip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let totalAcceleratedHashrate = 0;
|
||||||
|
for (const poolId of (this.accelerationInfo?.pools || this.tx.acceleratedBy || [])) {
|
||||||
|
const pool = pools[poolId];
|
||||||
|
if (!pool) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
totalAcceleratedHashrate += parseFloat(pool.lastEstimatedHashrate);
|
||||||
|
}
|
||||||
|
this.acceleratedByPercentage = ((totalAcceleratedHashrate / parseFloat(this.miningStats.lastEstimatedHashrate)) * 100).toFixed(1) + '%';
|
||||||
|
data.push(getDataItem(
|
||||||
|
totalAcceleratedHashrate,
|
||||||
|
'var(--tertiary)',
|
||||||
|
`${this.acceleratedByPercentage} accelerating`,
|
||||||
|
) as PieSeriesOption);
|
||||||
|
const notAcceleratedByPercentage = ((1 - (totalAcceleratedHashrate / parseFloat(this.miningStats.lastEstimatedHashrate))) * 100).toFixed(1) + '%';
|
||||||
|
data.push(getDataItem(
|
||||||
|
(parseFloat(this.miningStats.lastEstimatedHashrate) - totalAcceleratedHashrate),
|
||||||
|
'rgba(127, 127, 127, 0.3)',
|
||||||
|
`${notAcceleratedByPercentage} not accelerating`,
|
||||||
|
) as PieSeriesOption);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareChartOptions() {
|
||||||
|
this.chartOptions = {
|
||||||
|
animation: false,
|
||||||
|
grid: {
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
show: true,
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: 'pie',
|
||||||
|
radius: '100%',
|
||||||
|
data: this.getChartData(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onChartInit(ec) {
|
||||||
|
if (this.chartInstance !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.chartInstance = ec;
|
||||||
|
}
|
||||||
|
}
|
|
@ -347,6 +347,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
if (txPosition.position?.accelerated) {
|
if (txPosition.position?.accelerated) {
|
||||||
this.tx.acceleration = true;
|
this.tx.acceleration = true;
|
||||||
|
this.tx.acceleratedBy = txPosition.position?.acceleratedBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (txPosition.position?.block === 0) {
|
if (txPosition.position?.block === 0) {
|
||||||
|
@ -602,6 +603,7 @@ export class TrackerComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
if (cpfpInfo.acceleration) {
|
if (cpfpInfo.acceleration) {
|
||||||
this.tx.acceleration = cpfpInfo.acceleration;
|
this.tx.acceleration = cpfpInfo.acceleration;
|
||||||
|
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cpfpInfo = cpfpInfo;
|
this.cpfpInfo = cpfpInfo;
|
||||||
|
|
|
@ -419,7 +419,11 @@
|
||||||
<ng-template #detailsRight>
|
<ng-template #detailsRight>
|
||||||
<ng-container *ngTemplateOutlet="feeRow"></ng-container>
|
<ng-container *ngTemplateOutlet="feeRow"></ng-container>
|
||||||
<ng-container *ngTemplateOutlet="feeRateRow"></ng-container>
|
<ng-container *ngTemplateOutlet="feeRateRow"></ng-container>
|
||||||
<ng-container *ngTemplateOutlet="effectiveRateRow"></ng-container>
|
@if (!isLoadingTx && !tx?.status?.confirmed && ((cpfpInfo && hasEffectiveFeeRate) || accelerationInfo)) {
|
||||||
|
<ng-container *ngTemplateOutlet="acceleratingRow"></ng-container>
|
||||||
|
} @else {
|
||||||
|
<ng-container *ngTemplateOutlet="effectiveRateRow"></ng-container>
|
||||||
|
}
|
||||||
@if (tx?.status?.confirmed) {
|
@if (tx?.status?.confirmed) {
|
||||||
<ng-container *ngTemplateOutlet="minerRow"></ng-container>
|
<ng-container *ngTemplateOutlet="minerRow"></ng-container>
|
||||||
}
|
}
|
||||||
|
@ -638,6 +642,15 @@
|
||||||
}
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #acceleratingRow>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="2" colspan="2" style="padding: 0;">
|
||||||
|
<app-active-acceleration-box [tx]="tx" [accelerationInfo]="accelerationInfo" [miningStats]="miningStats"></app-active-acceleration-box>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr></tr>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #minerRow>
|
<ng-template #minerRow>
|
||||||
@if (network === '') {
|
@if (network === '') {
|
||||||
@if (!isLoadingTx) {
|
@if (!isLoadingTx) {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { isFeatureActive } from '../../bitcoin.utils';
|
||||||
import { ServicesApiServices } from '../../services/services-api.service';
|
import { ServicesApiServices } from '../../services/services-api.service';
|
||||||
import { EnterpriseService } from '../../services/enterprise.service';
|
import { EnterpriseService } from '../../services/enterprise.service';
|
||||||
import { ZONE_SERVICE } from '../../injection-tokens';
|
import { ZONE_SERVICE } from '../../injection-tokens';
|
||||||
|
import { MiningService, MiningStats } from '../../services/mining.service';
|
||||||
|
|
||||||
interface Pool {
|
interface Pool {
|
||||||
id: number;
|
id: number;
|
||||||
|
@ -98,6 +99,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
isAcceleration: boolean = false;
|
isAcceleration: boolean = false;
|
||||||
filters: Filter[] = [];
|
filters: Filter[] = [];
|
||||||
showCpfpDetails = false;
|
showCpfpDetails = false;
|
||||||
|
miningStats: MiningStats;
|
||||||
fetchCpfp$ = new Subject<string>();
|
fetchCpfp$ = new Subject<string>();
|
||||||
fetchRbfHistory$ = new Subject<string>();
|
fetchRbfHistory$ = new Subject<string>();
|
||||||
fetchCachedTx$ = new Subject<string>();
|
fetchCachedTx$ = new Subject<string>();
|
||||||
|
@ -151,6 +153,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
private priceService: PriceService,
|
private priceService: PriceService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private enterpriseService: EnterpriseService,
|
private enterpriseService: EnterpriseService,
|
||||||
|
private miningService: MiningService,
|
||||||
private cd: ChangeDetectorRef,
|
private cd: ChangeDetectorRef,
|
||||||
@Inject(ZONE_SERVICE) private zoneService: any,
|
@Inject(ZONE_SERVICE) private zoneService: any,
|
||||||
) {}
|
) {}
|
||||||
|
@ -696,6 +699,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
}
|
}
|
||||||
if (cpfpInfo.acceleration) {
|
if (cpfpInfo.acceleration) {
|
||||||
this.tx.acceleration = cpfpInfo.acceleration;
|
this.tx.acceleration = cpfpInfo.acceleration;
|
||||||
|
this.tx.acceleratedBy = cpfpInfo.acceleratedBy;
|
||||||
this.setIsAccelerated(firstCpfp);
|
this.setIsAccelerated(firstCpfp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,6 +717,12 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
if (this.isAcceleration && initialState) {
|
if (this.isAcceleration && initialState) {
|
||||||
this.showAccelerationSummary = false;
|
this.showAccelerationSummary = false;
|
||||||
}
|
}
|
||||||
|
if (this.isAcceleration) {
|
||||||
|
// this immediately returns cached stats if we fetched them recently
|
||||||
|
this.miningService.getMiningStats('1w').subscribe(stats => {
|
||||||
|
this.miningStats = stats;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFeatures(): void {
|
setFeatures(): void {
|
||||||
|
@ -790,6 +800,20 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||||
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
|
return +(cpfpTx.fee / (cpfpTx.weight / 4)).toFixed(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUnacceleratedFeeRate(tx: Transaction, accelerated: boolean): number {
|
||||||
|
if (accelerated) {
|
||||||
|
let ancestorVsize = tx.weight / 4;
|
||||||
|
let ancestorFee = tx.fee;
|
||||||
|
for (const ancestor of tx.ancestors || []) {
|
||||||
|
ancestorVsize += (ancestor.weight / 4);
|
||||||
|
ancestorFee += ancestor.fee;
|
||||||
|
}
|
||||||
|
return Math.min(tx.fee / (tx.weight / 4), (ancestorFee / ancestorVsize));
|
||||||
|
} else {
|
||||||
|
return tx.effectiveFeePerVsize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setupGraph() {
|
setupGraph() {
|
||||||
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1));
|
||||||
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
this.graphHeight = this.graphExpanded ? this.maxInOut * 15 : Math.min(360, this.maxInOut * 80);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Routes, RouterModule } from '@angular/router';
|
||||||
import { TransactionComponent } from './transaction.component';
|
import { TransactionComponent } from './transaction.component';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module';
|
import { TxBowtieModule } from '../tx-bowtie-graph/tx-bowtie.module';
|
||||||
|
import { GraphsModule } from '../../graphs/graphs.module';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -30,6 +31,7 @@ export class TransactionRoutingModule { }
|
||||||
CommonModule,
|
CommonModule,
|
||||||
TransactionRoutingModule,
|
TransactionRoutingModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
|
GraphsModule,
|
||||||
TxBowtieModule,
|
TxBowtieModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
|
|
@ -36,6 +36,7 @@ import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools
|
||||||
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component';
|
||||||
import { AddressComponent } from '../components/address/address.component';
|
import { AddressComponent } from '../components/address/address.component';
|
||||||
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
|
import { AddressGraphComponent } from '../components/address-graph/address-graph.component';
|
||||||
|
import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -75,6 +76,7 @@ import { CommonModule } from '@angular/common';
|
||||||
HashrateChartPoolsComponent,
|
HashrateChartPoolsComponent,
|
||||||
BlockHealthGraphComponent,
|
BlockHealthGraphComponent,
|
||||||
AddressGraphComponent,
|
AddressGraphComponent,
|
||||||
|
ActiveAccelerationBox,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -86,6 +88,7 @@ import { CommonModule } from '@angular/common';
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
NgxEchartsModule,
|
NgxEchartsModule,
|
||||||
|
ActiveAccelerationBox,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class GraphsModule { }
|
export class GraphsModule { }
|
||||||
|
|
|
@ -20,6 +20,7 @@ export interface Transaction {
|
||||||
bestDescendant?: BestDescendant | null;
|
bestDescendant?: BestDescendant | null;
|
||||||
cpfpChecked?: boolean;
|
cpfpChecked?: boolean;
|
||||||
acceleration?: boolean;
|
acceleration?: boolean;
|
||||||
|
acceleratedBy?: number[];
|
||||||
deleteAfter?: number;
|
deleteAfter?: number;
|
||||||
_unblinded?: any;
|
_unblinded?: any;
|
||||||
_deduced?: boolean;
|
_deduced?: boolean;
|
||||||
|
|
|
@ -29,6 +29,7 @@ export interface CpfpInfo {
|
||||||
sigops?: number;
|
sigops?: number;
|
||||||
adjustedVsize?: number;
|
adjustedVsize?: number;
|
||||||
acceleration?: boolean;
|
acceleration?: boolean;
|
||||||
|
acceleratedBy?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RbfInfo {
|
export interface RbfInfo {
|
||||||
|
@ -132,6 +133,7 @@ export interface ITranslators { [language: string]: string; }
|
||||||
*/
|
*/
|
||||||
export interface SinglePoolStats {
|
export interface SinglePoolStats {
|
||||||
poolId: number;
|
poolId: number;
|
||||||
|
poolUniqueId: number; // unique global pool id
|
||||||
name: string;
|
name: string;
|
||||||
link: string;
|
link: string;
|
||||||
blockCount: number;
|
blockCount: number;
|
||||||
|
@ -245,7 +247,8 @@ export interface RbfTransaction extends TransactionStripped {
|
||||||
export interface MempoolPosition {
|
export interface MempoolPosition {
|
||||||
block: number,
|
block: number,
|
||||||
vsize: number,
|
vsize: number,
|
||||||
accelerated?: boolean
|
accelerated?: boolean,
|
||||||
|
acceleratedBy?: number[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RewardStats {
|
export interface RewardStats {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map, tap } from 'rxjs/operators';
|
||||||
import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface';
|
import { PoolsStats, SinglePoolStats } from '../interfaces/node-api.interface';
|
||||||
import { ApiService } from '../services/api.service';
|
import { ApiService } from '../services/api.service';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
|
@ -25,6 +25,12 @@ export interface MiningStats {
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class MiningService {
|
export class MiningService {
|
||||||
|
cache: {
|
||||||
|
[interval: string]: {
|
||||||
|
lastUpdated: number;
|
||||||
|
data: MiningStats;
|
||||||
|
}
|
||||||
|
} = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
@ -36,9 +42,20 @@ export class MiningService {
|
||||||
* Generate pool ranking stats
|
* Generate pool ranking stats
|
||||||
*/
|
*/
|
||||||
public getMiningStats(interval: string): Observable<MiningStats> {
|
public getMiningStats(interval: string): Observable<MiningStats> {
|
||||||
return this.apiService.listPools$(interval).pipe(
|
// returned cached data fetched within the last 5 minutes
|
||||||
map(response => this.generateMiningStats(response))
|
if (this.cache[interval] && this.cache[interval].lastUpdated > (Date.now() - (5 * 60000))) {
|
||||||
);
|
return of(this.cache[interval].data);
|
||||||
|
} else {
|
||||||
|
return this.apiService.listPools$(interval).pipe(
|
||||||
|
map(response => this.generateMiningStats(response)),
|
||||||
|
tap(stats => {
|
||||||
|
this.cache[interval] = {
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
data: stats,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Reference in a new issue