Merge branch 'master' into nymkappa/bugfix/indexing-message
This commit is contained in:
@@ -4,7 +4,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators';
|
||||
import { Transaction, Vout } from '../../interfaces/electrs.interface';
|
||||
import { Observable, of, Subscription, asyncScheduler } from 'rxjs';
|
||||
import { Observable, of, Subscription, asyncScheduler, EMPTY } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
@@ -142,8 +142,21 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
this.location.replaceState(
|
||||
this.router.createUrlTree([(this.network ? '/' + this.network : '') + '/block/', hash]).toString()
|
||||
);
|
||||
return this.apiService.getBlock$(hash);
|
||||
})
|
||||
return this.apiService.getBlock$(hash).pipe(
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.isLoadingBlock = false;
|
||||
this.isLoadingOverview = false;
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.isLoadingBlock = false;
|
||||
this.isLoadingOverview = false;
|
||||
return EMPTY;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,7 +165,14 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
return of(blockInCache);
|
||||
}
|
||||
|
||||
return this.apiService.getBlock$(blockHash);
|
||||
return this.apiService.getBlock$(blockHash).pipe(
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
this.isLoadingBlock = false;
|
||||
this.isLoadingOverview = false;
|
||||
return EMPTY;
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
tap((block: BlockExtended) => {
|
||||
@@ -168,7 +188,6 @@ export class BlockComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.block = block;
|
||||
this.blockHeight = block.height;
|
||||
const direction = (this.lastBlockHeight < this.blockHeight) ? 'right' : 'left';
|
||||
this.lastBlockHeight = this.blockHeight;
|
||||
this.nextBlockHeight = block.height + 1;
|
||||
this.setNextAndPreviousBlockLink();
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="col">
|
||||
<table class="table table-borderless smaller-text table-sm table-tx-vin">
|
||||
<tbody>
|
||||
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length>12)?tx.vin.slice(0, 10): tx.vin.slice(0, 12)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
||||
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? ((tx.vin.length > rowLimit) ? tx.vin.slice(0, rowLimit - 2) : tx.vin.slice(0, rowLimit)) : tx.vin" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded,
|
||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
||||
@@ -146,9 +146,9 @@
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
|
||||
<tr *ngIf="tx.vin.length > rowLimit && tx['@vinLimit']">
|
||||
<td colspan="3" class="text-center">
|
||||
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length - 10 }})</button>
|
||||
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length }})</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -158,7 +158,7 @@
|
||||
<div class="col mobile-bottomcol">
|
||||
<table class="table table-borderless smaller-text table-sm table-tx-vout">
|
||||
<tbody>
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > 12) ? tx.vout.slice(0, 10) : tx.vout.slice(0, 12)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
|
||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] && !outputIndex ? ((tx.vout.length > rowLimit) ? tx.vout.slice(0, rowLimit - 2) : tx.vout.slice(0, rowLimit)) : tx.vout" [ngForTrackBy]="trackByIndexFn">
|
||||
<tr [ngClass]="{
|
||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
||||
@@ -257,9 +257,9 @@
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<tr *ngIf="tx.vout.length > 12 && tx['@voutLimit'] && !outputIndex">
|
||||
<tr *ngIf="tx.vout.length > rowLimit && tx['@voutLimit'] && !outputIndex">
|
||||
<td colspan="3" class="text-center">
|
||||
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length - 10 }})</button>
|
||||
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@voutLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vout.length }})</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -26,6 +26,8 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
@Input() paginated = false;
|
||||
@Input() outputIndex: number;
|
||||
@Input() address: string = '';
|
||||
@Input() rowLimit = 12;
|
||||
@Input() channels: { inputs: any[], outputs: any[] };
|
||||
|
||||
@Output() loadMore = new EventEmitter();
|
||||
|
||||
@@ -36,7 +38,6 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
showDetails$ = new BehaviorSubject<boolean>(false);
|
||||
outspends: Outspend[][] = [];
|
||||
assetsMinimal: any;
|
||||
channels: { inputs: any[], outputs: any[] };
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@@ -127,7 +128,9 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
||||
});
|
||||
const txIds = this.transactions.map((tx) => tx.txid);
|
||||
this.refreshOutspends$.next(txIds);
|
||||
this.refreshChannels$.next(txIds);
|
||||
if (!this.channels) {
|
||||
this.refreshChannels$.next(txIds);
|
||||
}
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<ng-template [ngIf]="!tx.status.confirmed || tx.status.block_height >= 477120">
|
||||
<span *ngIf="segwitGains.realizedSegwitGains && !segwitGains.potentialSegwitGains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number: '1.0-0' }}% on fees by using native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
|
||||
<ng-template #segwitTwo>
|
||||
<span *ngIf="segwitGains.realizedSegwitGains && segwitGains.potentialSegwitGains; else potentialP2shSegwitGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number: '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
|
||||
@@ -5,17 +6,22 @@
|
||||
<span *ngIf="segwitGains.potentialP2shSegwitGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit or {{ segwitGains.potentialP2shSegwitGains * 100 | number: '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy and fees saved with taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<ng-template [ngIf]="!tx.status.confirmed || tx.status.block_height >= 709632">
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about fees saved with taproot" ngbTooltip="This transaction uses Taproot and thereby saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<ng-template #notFullyTaproot>
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about privacy and more fees that could be saved with more taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about fees that saved and could be saved with taproot" ngbTooltip="This transaction uses Taproot and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<ng-template #noTaproot>
|
||||
<span *ngIf="segwitGains.potentialTaprootGains; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about privacy and fees that could be saved with taproot" ngbTooltip="This transaction could increase the user's privacy and save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
|
||||
<span *ngIf="segwitGains.potentialTaprootGains; else taprootButNoGains" class="badge badge-danger mr-1" i18n-ngbTooltip="Tooltip about fees that could be saved with taproot" ngbTooltip="This transaction could save {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% on fees by using Taproot" placement="bottom"><del i18n="tx-features.tag.taproot|Taproot">Taproot</del></span>
|
||||
<ng-template #taprootButNoGains>
|
||||
<span *ngIf="isTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy with taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
<span *ngIf="isTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about taproot" ngbTooltip="This transaction uses Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<ng-template [ngIf]="!tx.status.confirmed || tx.status.block_height > 399700">
|
||||
<span *ngIf="isRbfTransaction; else rbfDisabled" class="badge badge-success" i18n-ngbTooltip="RBF tooltip" ngbTooltip="This transaction supports Replace-By-Fee (RBF) allowing fee bumping" placement="bottom" i18n="tx-features.tag.rbf|RBF">RBF</span>
|
||||
<ng-template #rbfDisabled><span class="badge badge-danger mr-1" i18n-ngbTooltip="RBF disabled tooltip" ngbTooltip="This transaction does NOT support Replace-By-Fee (RBF) and cannot be fee bumped using this method" placement="bottom"><del i18n="tx-features.tag.rbf|RBF">RBF</del></span></ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -151,3 +151,41 @@ export interface RewardStats {
|
||||
totalFee: number;
|
||||
totalTx: number;
|
||||
}
|
||||
|
||||
export interface ITopNodesPerChannels {
|
||||
publicKey: string,
|
||||
alias: string,
|
||||
channels?: number,
|
||||
capacity: number,
|
||||
firstSeen?: number,
|
||||
updatedAt?: number,
|
||||
city?: any,
|
||||
country?: any,
|
||||
}
|
||||
|
||||
export interface ITopNodesPerCapacity {
|
||||
publicKey: string,
|
||||
alias: string,
|
||||
capacity: number,
|
||||
channels?: number,
|
||||
firstSeen?: number,
|
||||
updatedAt?: number,
|
||||
city?: any,
|
||||
country?: any,
|
||||
}
|
||||
|
||||
export interface INodesRanking {
|
||||
topByCapacity: ITopNodesPerCapacity[];
|
||||
topByChannels: ITopNodesPerChannels[];
|
||||
}
|
||||
|
||||
export interface IOldestNodes {
|
||||
publicKey: string,
|
||||
alias: string,
|
||||
firstSeen: number,
|
||||
channels?: number,
|
||||
capacity: number,
|
||||
updatedAt?: number,
|
||||
city?: any,
|
||||
country?: any,
|
||||
}
|
||||
@@ -16,3 +16,9 @@
|
||||
color: #ffffff66;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
|
||||
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
|
||||
|
||||
<div class="box">
|
||||
|
||||
@@ -30,32 +30,6 @@
|
||||
<td i18n="address.total-sent">Last update</td>
|
||||
<td><app-timestamp [dateString]="channel.updated_at"></app-timestamp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent">Opening transaction</td>
|
||||
<td>
|
||||
<a [routerLink]="['/tx' | relativeUrl, channel.transaction_id + ':' + channel.transaction_vout]" >
|
||||
<span>{{ channel.transaction_id | shortenString : 10 }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="channel.transaction_id"></app-clipboard>
|
||||
</td>
|
||||
</tr>
|
||||
<ng-template [ngIf]="channel.closing_transaction_id">
|
||||
<tr *ngIf="channel.closing_transaction_id">
|
||||
<td i18n="address.total-sent">Closing transaction</td>
|
||||
<td>
|
||||
<a [routerLink]="['/tx' | relativeUrl, channel.closing_transaction_id]" >
|
||||
<span>{{ channel.closing_transaction_id | shortenString : 10 }}</span>
|
||||
</a>
|
||||
<app-clipboard [text]="channel.closing_transaction_id"></app-clipboard>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="address.total-sent">Closing type</td>
|
||||
<td>
|
||||
<app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -82,8 +56,23 @@
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_right"></app-channel-box>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="transactions$ | async as transactions">
|
||||
<ng-template [ngIf]="transactions[0]">
|
||||
<h3>Opening transaction</h3>
|
||||
<app-transactions-list [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [], outputs: [channel] }"></app-transactions-list>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="transactions[1]">
|
||||
<div class="closing-header">
|
||||
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||
</div>
|
||||
<app-transactions-list [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5" [channels]="{ inputs: [channel], outputs: [] }"></app-transactions-list>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,3 +39,16 @@ app-fiat {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.closing-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h3 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||
import { forkJoin, Observable, of, share, zip } from 'rxjs';
|
||||
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@@ -13,13 +15,15 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
})
|
||||
export class ChannelComponent implements OnInit {
|
||||
channel$: Observable<any>;
|
||||
channelGeo$: Observable<number[]>;
|
||||
transactions$: Observable<any>;
|
||||
error: any = null;
|
||||
channelGeo: number[] = [];
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -30,28 +34,41 @@ export class ChannelComponent implements OnInit {
|
||||
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
|
||||
return this.lightningApiService.getChannel$(params.get('short_id'))
|
||||
.pipe(
|
||||
tap((data) => {
|
||||
if (!data.node_left.longitude || !data.node_left.latitude ||
|
||||
!data.node_right.longitude || !data.node_right.latitude) {
|
||||
this.channelGeo = [];
|
||||
} else {
|
||||
this.channelGeo = [
|
||||
data.node_left.public_key,
|
||||
data.node_left.alias,
|
||||
data.node_left.longitude, data.node_left.latitude,
|
||||
data.node_right.public_key,
|
||||
data.node_right.alias,
|
||||
data.node_right.longitude, data.node_right.latitude,
|
||||
];
|
||||
}
|
||||
}),
|
||||
catchError((err) => {
|
||||
this.error = err;
|
||||
return of(null);
|
||||
})
|
||||
);
|
||||
})
|
||||
}),
|
||||
shareReplay(),
|
||||
);
|
||||
|
||||
this.channelGeo$ = this.channel$.pipe(
|
||||
map((data) => {
|
||||
if (!data.node_left.longitude || !data.node_left.latitude ||
|
||||
!data.node_right.longitude || !data.node_right.latitude) {
|
||||
return [];
|
||||
} else {
|
||||
return [
|
||||
data.node_left.public_key,
|
||||
data.node_left.alias,
|
||||
data.node_left.longitude, data.node_left.latitude,
|
||||
data.node_right.public_key,
|
||||
data.node_right.alias,
|
||||
data.node_right.longitude, data.node_right.latitude,
|
||||
];
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
this.transactions$ = this.channel$.pipe(
|
||||
switchMap((data) => {
|
||||
return zip([
|
||||
data.transaction_id ? this.electrsApiService.getTransaction$(data.transaction_id) : of(null),
|
||||
data.closing_transaction_id ? this.electrsApiService.getTransaction$(data.closing_transaction_id) : of(null),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="response.channels.length > 0" class="pagination-container float-right" [size]="paginationSize" [collectionSize]="response.totalItems" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||
<ngb-pagination *ngIf="response.channels.length > 0" class="pagination-container float-right"
|
||||
[size]="paginationSize" [collectionSize]="response.totalItems" [rotate]="true"
|
||||
[pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)"
|
||||
[maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
|
||||
<table class="table table-borderless" *ngIf="response.channels.length === 0">
|
||||
<div class="d-flex justify-content-center" i18n="lightning.empty-channels-list">No channels to display</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false });
|
||||
this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: true });
|
||||
this.channelsPage$.next(1);
|
||||
|
||||
this.channels$ = merge(
|
||||
@@ -70,7 +70,7 @@ export class ChannelsListComponent implements OnInit, OnChanges {
|
||||
map((response) => {
|
||||
return {
|
||||
channels: response.body,
|
||||
totalItems: parseInt(response.headers.get('x-total-count'), 10) + 1
|
||||
totalItems: parseInt(response.headers.get('x-total-count'), 10)
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from '../services/state.service';
|
||||
import { INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -48,8 +49,8 @@ export class LightningApiService {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||
}
|
||||
|
||||
listTopNodes$(): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/top');
|
||||
getNodesRanking$(): Observable<INodesRanking> {
|
||||
return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
|
||||
}
|
||||
|
||||
listChannelStats$(publicKey: string): Observable<any> {
|
||||
@@ -62,4 +63,22 @@ export class LightningApiService {
|
||||
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
|
||||
);
|
||||
}
|
||||
|
||||
getTopNodesByCapacity$(): Observable<ITopNodesPerCapacity[]> {
|
||||
return this.httpClient.get<ITopNodesPerCapacity[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/capacity'
|
||||
);
|
||||
}
|
||||
|
||||
getTopNodesByChannels$(): Observable<ITopNodesPerChannels[]> {
|
||||
return this.httpClient.get<ITopNodesPerChannels[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/channels'
|
||||
);
|
||||
}
|
||||
|
||||
getOldestNodes$(): Observable<IOldestNodes[]> {
|
||||
return this.httpClient.get<IOldestNodes[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network history -->
|
||||
<div class="col">
|
||||
<div class="card graph-card">
|
||||
<div class="card-body pl-2 pr-2 pt-1">
|
||||
@@ -53,22 +54,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top nodes per capacity -->
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Top Capacity Nodes</h5>
|
||||
<app-nodes-list [nodes$]="nodesByCapacity$" [show]="'mobile-capacity'"></app-nodes-list>
|
||||
<!-- <div><a [routerLink]="['/lightning/nodes' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> -->
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-capacity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.top-capacity-nodes">Top capacity nodes</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top nodes per channels -->
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Most Connected Nodes</h5>
|
||||
<app-nodes-list [nodes$]="nodesByChannels$" [show]="'mobile-channels'"></app-nodes-list>
|
||||
<!-- <div><a [routerLink]="['/lightning/nodes' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> -->
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-channels' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.top-channels-nodes">Most connected nodes</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share } from 'rxjs/operators';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { INodesRanking } from 'src/app/interfaces/node-api.interface';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@@ -11,9 +12,8 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LightningDashboardComponent implements OnInit {
|
||||
nodesByCapacity$: Observable<any>;
|
||||
nodesByChannels$: Observable<any>;
|
||||
statistics$: Observable<any>;
|
||||
nodesRanking$: Observable<INodesRanking>;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
@@ -23,18 +23,7 @@ export class LightningDashboardComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning Dashboard`);
|
||||
|
||||
const sharedObservable = this.lightningApiService.listTopNodes$().pipe(share());
|
||||
|
||||
this.nodesByCapacity$ = sharedObservable
|
||||
.pipe(
|
||||
map((object) => object.topByCapacity),
|
||||
);
|
||||
|
||||
this.nodesByChannels$ = sharedObservable
|
||||
.pipe(
|
||||
map((object) => object.topByChannels),
|
||||
);
|
||||
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,12 @@ import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
|
||||
import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component';
|
||||
import { NodesMap } from '../lightning/nodes-map/nodes-map.component';
|
||||
import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels-map.component';
|
||||
import { NodesRanking } from '../lightning/nodes-ranking/nodes-ranking.component';
|
||||
import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component';
|
||||
import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component';
|
||||
import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
|
||||
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
LightningDashboardComponent,
|
||||
@@ -45,6 +51,11 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels
|
||||
NodesPerCountryChartComponent,
|
||||
NodesMap,
|
||||
NodesChannelsMap,
|
||||
NodesRanking,
|
||||
TopNodesPerChannels,
|
||||
TopNodesPerCapacity,
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -73,6 +84,11 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels
|
||||
NodesPerCountryChartComponent,
|
||||
NodesMap,
|
||||
NodesChannelsMap,
|
||||
NodesRanking,
|
||||
TopNodesPerChannels,
|
||||
TopNodesPerCapacity,
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
],
|
||||
providers: [
|
||||
LightningApiService,
|
||||
|
||||
@@ -6,6 +6,8 @@ import { NodeComponent } from './node/node.component';
|
||||
import { ChannelComponent } from './channel/channel.component';
|
||||
import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component';
|
||||
import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
|
||||
import { NodesRanking } from './nodes-ranking/nodes-ranking.component';
|
||||
import { NodesRankingsDashboard } from './nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -32,6 +34,31 @@ const routes: Routes = [
|
||||
path: 'nodes/isp/:isp',
|
||||
component: NodesPerISP,
|
||||
},
|
||||
{
|
||||
path: 'nodes/rankings',
|
||||
component: NodesRankingsDashboard,
|
||||
},
|
||||
{
|
||||
path: 'nodes/top-capacity',
|
||||
component: NodesRanking,
|
||||
data: {
|
||||
type: 'capacity'
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'nodes/top-channels',
|
||||
component: NodesRanking,
|
||||
data: {
|
||||
type: 'channels'
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'nodes/oldest',
|
||||
component: NodesRanking,
|
||||
data: {
|
||||
type: 'oldest'
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
|
||||
@@ -154,7 +154,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: () => {
|
||||
return `<b style="color: white">${isp[1]} (${isp[6]}%)</b><br>` +
|
||||
return `<b style="color: white">${isp[1]} (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)</b><br>` +
|
||||
$localize`${isp[4].toString()} nodes<br>` +
|
||||
$localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
|
||||
;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<app-top-nodes-per-capacity [nodes$]="null" [widget]="false" *ngIf="type === 'capacity'">
|
||||
</app-top-nodes-per-capacity>
|
||||
|
||||
<app-top-nodes-per-channels [nodes$]="null" [widget]="false" *ngIf="type === 'channels'">
|
||||
</app-top-nodes-per-channels>
|
||||
|
||||
<app-oldest-nodes [widget]="false" *ngIf="type === 'oldest'"></app-oldest-nodes>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-ranking',
|
||||
templateUrl: './nodes-ranking.component.html',
|
||||
styleUrls: ['./nodes-ranking.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesRanking implements OnInit {
|
||||
type: string;
|
||||
|
||||
constructor(private route: ActivatedRoute) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.data.subscribe(data => {
|
||||
this.type = data.type;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<div [class]="!widget ? 'container-xl full-height' : ''">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-oldest-nodes">
|
||||
<span>Top 100 oldest lightning nodes</span>
|
||||
</h1>
|
||||
|
||||
<div [class]="widget ? 'widget' : 'full'">
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="rank"></th>
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
<th class="timestamp-first text-right" i18n="lightning.first_seen">First seen</th>
|
||||
<th *ngIf="!widget" class="capacity text-right" i18n="node.capacity">Capacity</th>
|
||||
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="oldestNodes$ | async as nodes; else skeleton">
|
||||
<tr *ngFor="let node of nodes; let i = index;">
|
||||
<td class="rank text-left">
|
||||
{{ i + 1 }}
|
||||
</td>
|
||||
<td class="alias text-left">
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">{{ node.alias }}</a>
|
||||
</td>
|
||||
<td class="timestamp-first text-right">
|
||||
‎{{ node.firstSeen * 1000 | date: 'yyyy-MM-dd' }}
|
||||
</td>
|
||||
<td *ngIf="!widget" class="capacity text-right">
|
||||
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="channels text-right">
|
||||
{{ node.channels | number }}
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||
{{ node?.city?.en ?? '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonRows">
|
||||
<td class="rank text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="alias text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="capacity text-right">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="channels text-right">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
padding-bottom: 100px;
|
||||
@media (min-width: 767.98px) {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.table td, .table th {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.full .rank {
|
||||
width: 5%;
|
||||
}
|
||||
.widget .rank {
|
||||
@media (min-width: 767.98px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .alias {
|
||||
width: 10%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
.widget .alias {
|
||||
width: 50%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 300px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-width: 170px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .capacity {
|
||||
width: 10%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.widget .capacity {
|
||||
width: 10%;
|
||||
@media (max-width: 767.98px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .channels {
|
||||
width: 15%;
|
||||
padding-right: 50px;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-first {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.full .timestamp-update {
|
||||
width: 20%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .location {
|
||||
width: 10%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { IOldestNodes } from '../../../interfaces/node-api.interface';
|
||||
import { LightningApiService } from '../../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-oldest-nodes',
|
||||
templateUrl: './oldest-nodes.component.html',
|
||||
styleUrls: ['./oldest-nodes.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class OldestNodes implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
oldestNodes$: Observable<IOldestNodes[]>;
|
||||
skeletonRows: number[] = [];
|
||||
|
||||
constructor(private apiService: LightningApiService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
|
||||
this.skeletonRows.push(i);
|
||||
}
|
||||
|
||||
if (this.widget === false) {
|
||||
this.oldestNodes$ = this.apiService.getOldestNodes$();
|
||||
} else {
|
||||
this.oldestNodes$ = this.apiService.getOldestNodes$().pipe(
|
||||
map((nodes: IOldestNodes[]) => {
|
||||
return nodes.slice(0, 10);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<div [class]="!widget ? 'container-xl full-height' : ''">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-capacity">
|
||||
<span>Top 100 nodes by capacity</span>
|
||||
</h1>
|
||||
|
||||
<div [class]="widget ? 'widget' : 'full'">
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="rank"></th>
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
<th class="capacity text-right" i18n="node.capacity">Capacity</th>
|
||||
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
|
||||
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="topNodesPerCapacity$ | async as nodes; else skeleton">
|
||||
<tr *ngFor="let node of nodes; let i = index;">
|
||||
<td class="rank text-left">
|
||||
{{ i + 1 }}
|
||||
</td>
|
||||
<td class="alias text-left">
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">{{ node.alias }}</a>
|
||||
</td>
|
||||
<td class="capacity text-right">
|
||||
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="channels text-right">
|
||||
{{ node.channels | number }}
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||
{{ node?.city?.en ?? '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonRows">
|
||||
<td class="rank text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="alias text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="capacity text-right">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="channels text-right">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
padding-bottom: 100px;
|
||||
@media (min-width: 767.98px) {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.table td, .table th {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.full .rank {
|
||||
width: 5%;
|
||||
}
|
||||
.widget .rank {
|
||||
@media (min-width: 767.98px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .alias {
|
||||
width: 10%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
.widget .alias {
|
||||
width: 55%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .capacity {
|
||||
width: 10%;
|
||||
}
|
||||
.widget .capacity {
|
||||
width: 32%;
|
||||
@media (max-width: 767.98px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .channels {
|
||||
width: 15%;
|
||||
padding-right: 50px;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-first {
|
||||
width: 15%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-update {
|
||||
width: 15%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .location {
|
||||
width: 10%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface';
|
||||
import { LightningApiService } from '../../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-top-nodes-per-capacity',
|
||||
templateUrl: './top-nodes-per-capacity.component.html',
|
||||
styleUrls: ['./top-nodes-per-capacity.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TopNodesPerCapacity implements OnInit {
|
||||
@Input() nodes$: Observable<INodesRanking>;
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
|
||||
skeletonRows: number[] = [];
|
||||
|
||||
constructor(private apiService: LightningApiService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
|
||||
this.skeletonRows.push(i);
|
||||
}
|
||||
|
||||
if (this.widget === false) {
|
||||
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$();
|
||||
} else {
|
||||
this.topNodesPerCapacity$ = this.nodes$.pipe(
|
||||
map((ranking) => {
|
||||
return ranking.topByCapacity.slice(0, 10);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<div [class]="!widget ? 'container-xl full-height' : ''">
|
||||
<h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-channel">
|
||||
<span>Top 100 nodes by channel count</span>
|
||||
</h1>
|
||||
|
||||
<div [class]="widget ? 'widget' : 'full'">
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="rank"></th>
|
||||
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||
<th class="channels text-right" i18n="node.channels">Channels</th>
|
||||
<th *ngIf="!widget" class="capacity text-right" i18n="lightning.capacity">Capacity</th>
|
||||
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
|
||||
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="topNodesPerChannels$ | async as nodes; else skeleton">
|
||||
<tr *ngFor="let node of nodes; let i = index;">
|
||||
<td class="rank text-left">
|
||||
{{ i + 1 }}
|
||||
</td>
|
||||
<td class="alias text-left">
|
||||
<a [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">{{ node.alias }}</a>
|
||||
</td>
|
||||
<td class="channels text-right">
|
||||
{{ node.channels | number }}
|
||||
</td>
|
||||
<td *ngIf="!widget" class="capacity text-right">
|
||||
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||
{{ node?.city?.en ?? '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonRows">
|
||||
<td class="rank text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="alias text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="channels text-right">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="capacity text-right">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,84 @@
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
padding-bottom: 100px;
|
||||
@media (min-width: 767.98px) {
|
||||
padding-left: 50px;
|
||||
padding-right: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.table td, .table th {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.full .rank {
|
||||
width: 5%;
|
||||
}
|
||||
.widget .rank {
|
||||
@media (min-width: 767.98px) {
|
||||
width: 13%;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .alias {
|
||||
width: 10%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
.widget .alias {
|
||||
width: 55%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 350px;
|
||||
@media (max-width: 767.98px) {
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .channels {
|
||||
width: 10%;
|
||||
}
|
||||
.widget .channels {
|
||||
width: 32%;
|
||||
@media (max-width: 767.98px) {
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.full .capacity {
|
||||
width: 15%;
|
||||
padding-right: 50px;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-first {
|
||||
width: 15%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .timestamp-update {
|
||||
width: 15%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.full .location {
|
||||
width: 10%;
|
||||
@media (max-width: 767.98px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface';
|
||||
import { LightningApiService } from '../../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-top-nodes-per-channels',
|
||||
templateUrl: './top-nodes-per-channels.component.html',
|
||||
styleUrls: ['./top-nodes-per-channels.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TopNodesPerChannels implements OnInit {
|
||||
@Input() nodes$: Observable<INodesRanking>;
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
topNodesPerChannels$: Observable<ITopNodesPerChannels[]>;
|
||||
skeletonRows: number[] = [];
|
||||
|
||||
constructor(private apiService: LightningApiService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
|
||||
this.skeletonRows.push(i);
|
||||
}
|
||||
|
||||
if (this.widget === false) {
|
||||
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$();
|
||||
} else {
|
||||
this.topNodesPerChannels$ = this.nodes$.pipe(
|
||||
map((ranking) => {
|
||||
return ranking.topByChannels.slice(0, 10);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<div class="container main">
|
||||
<div class="row row-cols-1 row-cols-md-3">
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-capacity' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.top-capacity-nodes">Top capacity nodes</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-channels' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.top-channels-nodes">Most connected nodes</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<a class="title-link" href="" [routerLink]="['/lightning/nodes/oldest' | relativeUrl]">
|
||||
<h5 class="card-title d-inline" i18n="lightning.top-channels-age">Oldest nodes</h5>
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true"
|
||||
style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||
</a>
|
||||
<app-oldest-nodes [widget]="true"></app-oldest-nodes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
.main {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.col {
|
||||
padding-bottom: 20px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #1d1f31;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
}
|
||||
.card-title > a {
|
||||
color: #4a68b9;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.title-link, .title-link:hover, .title-link:focus, .title-link:active {
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable, share } from 'rxjs';
|
||||
import { INodesRanking } from 'src/app/interfaces/node-api.interface';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-rankings-dashboard',
|
||||
templateUrl: './nodes-rankings-dashboard.component.html',
|
||||
styleUrls: ['./nodes-rankings-dashboard.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesRankingsDashboard implements OnInit {
|
||||
nodesRanking$: Observable<INodesRanking>;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private seoService: SeoService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Top lightning nodes`);
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user