Merge branch 'master' into nymkappa/feature/zero-base-fee-tag
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<div class="box preview-box" *ngIf="(channel$ | async) as channel">
|
||||
<h2 class="preview-header" i18n="lightning.channel">lightning channel</h2>
|
||||
<app-preview-title>
|
||||
<span i18n="lightning.channel">lightning channel</span>
|
||||
</app-preview-title>
|
||||
<div class="row d-flex justify-content-between full-width-row">
|
||||
<div class="title-wrapper">
|
||||
<h1 class="title">{{ channel.short_id }}</h1>
|
||||
@@ -49,7 +51,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md map-col">
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" [disableSpinner]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row d-flex justify-content-between full-width-row nodes">
|
||||
|
||||
@@ -2,8 +2,8 @@ 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 { SeoService } from 'src/app/services/seo.service';
|
||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [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">
|
||||
|
||||
@@ -51,38 +52,42 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_left"></app-channel-box>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2">
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_left"></app-channel-box>
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_right"></app-channel-box>
|
||||
</div>
|
||||
<div class="col">
|
||||
<app-channel-box [channel]="channel.node_right"></app-channel-box>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="transactions$ | async as transactions">
|
||||
<ng-template [ngIf]="transactions[0]">
|
||||
<div class="d-flex">
|
||||
<h3>Opening transaction</h3>
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="transactions[1]">
|
||||
<div class="closing-header d-flex">
|
||||
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<br>
|
||||
|
||||
<ng-container *ngIf="transactions$ | async as transactions">
|
||||
<ng-template [ngIf]="transactions[0]">
|
||||
<div class="d-flex">
|
||||
<h3>Opening transaction</h3>
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()"
|
||||
i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5">
|
||||
</app-transactions-list>
|
||||
</ng-template>
|
||||
<ng-template [ngIf]="transactions[1]">
|
||||
<div class="closing-header d-flex">
|
||||
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason">
|
||||
</app-closing-type>
|
||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()"
|
||||
i18n="transaction.details|Transaction Details">Details</button>
|
||||
</div>
|
||||
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5">
|
||||
</app-transactions-list>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -108,7 +113,7 @@
|
||||
<div class="badges mb-2">
|
||||
<span class="skeleton-loader" style="width: 50px; height: 22px; margin-top: 5px;"></span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="height: 413px; padding: 15px;">
|
||||
@@ -152,4 +157,4 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Observable, of, zip } from 'rxjs';
|
||||
import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
|
||||
import { IChannel } from 'src/app/interfaces/node-api.interface';
|
||||
import { ElectrsApiService } from 'src/app/services/electrs-api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { IChannel } from '../../interfaces/node-api.interface';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnI
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { BehaviorSubject, merge, Observable } from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
|
||||
131
frontend/src/app/lightning/group/group.component.html
Normal file
131
frontend/src/app/lightning/group/group.component.html
Normal file
@@ -0,0 +1,131 @@
|
||||
<div class="container-xl full-height" style="min-height: 335px">
|
||||
<h5 class="mb-1" style="color: #ffffff66" i18n="lightning.node">Lightning node group</h5>
|
||||
|
||||
<div class="header">
|
||||
<div class="logo-container">
|
||||
<app-svg-images name="officialMempoolSpace" viewBox="0 0 125 126"></app-svg-images>
|
||||
</div>
|
||||
<h1>The Mempool Open Source Project</h1>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<div class="row" *ngIf="nodes$ | async as nodes">
|
||||
<div class="col-12 col-md-6">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td><div class="description-text">These are the Lightning nodes operated by The Mempool Open Source Project that provide data for the mempool.space website. Connect to us!
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.node-count">Nodes</td>
|
||||
<td>{{ nodes.nodes.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.liquidity">Liquidity</td>
|
||||
<td>
|
||||
<app-amount *ngIf="nodes.sumLiquidity > 100000000; else smallnode" [satoshis]="nodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
{{ nodes.sumLiquidity | amountShortener: 1 }}
|
||||
<span class="sats" i18n="shared.sats">sats</span>
|
||||
</ng-template>
|
||||
<span class="d-none d-md-inline-block"> </span>
|
||||
<span class="d-block d-md-none"></span>
|
||||
<app-fiat [value]="nodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.channels">Channels</td>
|
||||
<td>{{ nodes.sumChannels }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 p-3 p-md-0 pr-md-3">
|
||||
<div style="background-color: #181b2d">
|
||||
<app-nodes-map [widget]="true" [nodes]="nodes.nodes" type="isp"></app-nodes-map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="toggle-holder">
|
||||
<form [formGroup]="socketToggleForm" class="formRadioGroup">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="socket">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="0">IPv4
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="1">IPv6
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="lightning.alias">Alias</th>
|
||||
<th class="text-left">Connect</th>
|
||||
<th class="city text-right d-none d-md-table-cell" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="nodes$ | async as response; else skeleton">
|
||||
<tr *ngFor="let node of response.nodes; let i = index; trackBy: trackByPublicKey">
|
||||
<td class="alias text-left">
|
||||
<div class="text-truncate">
|
||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
<div class="second-line">{{ node.opened_channel_count }} channel(s), <app-amount *ngIf="node.capacity > 100000000; else smallnode" [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
{{ node.capacity | amountShortener: 1 }} <span class="sats" i18n="shared.sats">sats</span>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="timestamp-first text-left">
|
||||
<div class="input-group" *ngIf="node.socketsObject.length">
|
||||
<ng-template #noDropdown>
|
||||
<span class="input-group-text" id="basic-addon3">{{ node.socketsObject[selectedSocketIndex].label }}</span>
|
||||
</ng-template>
|
||||
<input type="text" class="form-control" aria-label="Text input with dropdown button"
|
||||
[value]="node.socketsObject[selectedSocketIndex].socket">
|
||||
<button class="btn btn-secondary ml-1" type="button" id="inputGroupFileAddon04" (mouseover)="qrCodeVisible[i] = 1"
|
||||
(mouseout)="qrCodeVisible[i] = 0">
|
||||
<fa-icon [icon]="['fas', 'qrcode']" [fixedWidth]="true"></fa-icon>
|
||||
<div class="qr-wrapper" [hidden]="!qrCodeVisible[i]">
|
||||
<app-qrcode [size]="200" [data]="node.socketsObject[selectedSocketIndex].socket"></app-qrcode>
|
||||
</div>
|
||||
</button>
|
||||
<button class="btn btn-secondary ml-1" type="button" id="inputGroupFileAddon04">
|
||||
<app-clipboard [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="city text-right text-truncate d-none d-md-table-cell">
|
||||
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
|
||||
</td>
|
||||
</tbody>
|
||||
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="alias">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="timestamp-update d-none d-md-table-cell">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
59
frontend/src/app/lightning/group/group.component.scss
Normal file
59
frontend/src/app/lightning/group/group.component.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
.logo-container {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
|
||||
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
left: -175px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dropdownLabel {
|
||||
min-width: 50px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#inputGroupFileAddon04 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-holder {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.text-truncate {
|
||||
width: 120px;
|
||||
}
|
||||
.btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.second-line {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
103
frontend/src/app/lightning/group/group.component.ts
Normal file
103
frontend/src/app/lightning/group/group.component.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { map, Observable, share } from 'rxjs';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-group',
|
||||
templateUrl: './group.component.html',
|
||||
styleUrls: ['./group.component.scss']
|
||||
})
|
||||
export class GroupComponent implements OnInit {
|
||||
nodes$: Observable<any>;
|
||||
isp: {name: string, id: number};
|
||||
|
||||
skeletonLines: number[] = [];
|
||||
selectedSocketIndex = 0;
|
||||
qrCodeVisible = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
socketToggleForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private seoService: SeoService,
|
||||
private formBuilder: FormBuilder,
|
||||
) {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
this.skeletonLines.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.socketToggleForm = this.formBuilder.group({
|
||||
socket: [this.selectedSocketIndex],
|
||||
});
|
||||
|
||||
this.socketToggleForm.get('socket').valueChanges.subscribe((val) => {
|
||||
this.selectedSocketIndex = val;
|
||||
});
|
||||
|
||||
this.seoService.setTitle(`Mempool.space Lightning Nodes`);
|
||||
|
||||
this.nodes$ = this.lightningApiService.getNodGroupNodes$('mempool.space')
|
||||
.pipe(
|
||||
map((nodes) => {
|
||||
for (const node of nodes) {
|
||||
const socketsObject = [];
|
||||
for (const socket of node.sockets.split(',')) {
|
||||
if (socket === '') {
|
||||
continue;
|
||||
}
|
||||
let label = '';
|
||||
if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) {
|
||||
label = 'IPv4';
|
||||
} else if (socket.indexOf('[') > -1) {
|
||||
label = 'IPv6';
|
||||
} else if (socket.indexOf('onion') > -1) {
|
||||
label = 'Tor';
|
||||
}
|
||||
socketsObject.push({
|
||||
label: label,
|
||||
socket: node.public_key + '@' + socket,
|
||||
});
|
||||
}
|
||||
// @ts-ignore
|
||||
node.socketsObject = socketsObject;
|
||||
|
||||
if (!node?.country && !node?.city &&
|
||||
!node?.subdivision) {
|
||||
// @ts-ignore
|
||||
node.geolocation = null;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.geolocation = <GeolocationData>{
|
||||
country: node.country?.en,
|
||||
city: node.city?.en,
|
||||
subdivision: node.subdivision?.en,
|
||||
iso: node.iso_code,
|
||||
};
|
||||
}
|
||||
}
|
||||
const sumLiquidity = nodes.reduce((partialSum, a) => partialSum + parseInt(a.capacity, 10), 0);
|
||||
const sumChannels = nodes.reduce((partialSum, a) => partialSum + a.opened_channel_count, 0);
|
||||
|
||||
return {
|
||||
nodes: nodes,
|
||||
sumLiquidity: sumLiquidity,
|
||||
sumChannels: sumChannels,
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
trackByPublicKey(index: number, node: any): string {
|
||||
return node.public_key;
|
||||
}
|
||||
|
||||
changeSocket(index: number) {
|
||||
this.selectedSocketIndex = index;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,10 @@ export class LightningApiService {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||
}
|
||||
|
||||
getNodGroupNodes$(name: string): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
|
||||
}
|
||||
|
||||
getChannel$(shortId: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
|
||||
}
|
||||
|
||||
@@ -84,3 +84,10 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-small text-center mt-1" *ngIf="officialMempoolSpace">
|
||||
<a [routerLink]="['/lightning/group/the-mempool-open-source-project' | relativeUrl]">Connect to our nodes</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { INodesRanking } from 'src/app/interfaces/node-api.interface';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { INodesRanking } from '../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
@@ -14,14 +15,16 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
export class LightningDashboardComponent implements OnInit {
|
||||
statistics$: Observable<any>;
|
||||
nodesRanking$: Observable<INodesRanking>;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private seoService: SeoService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning Dashboard`);
|
||||
this.seoService.setTitle($localize`Lightning Network`);
|
||||
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
|
||||
@@ -8,10 +8,12 @@ import { LightningApiService } from './lightning-api.service';
|
||||
import { NodePreviewComponent } from './node/node-preview.component';
|
||||
import { LightningPreviewsRoutingModule } from './lightning-previews.routing.module';
|
||||
import { ChannelPreviewComponent } from './channel/channel-preview.component';
|
||||
import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component';
|
||||
@NgModule({
|
||||
declarations: [
|
||||
NodePreviewComponent,
|
||||
ChannelPreviewComponent,
|
||||
NodesPerISPPreview,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { NodePreviewComponent } from './node/node-preview.component';
|
||||
import { ChannelPreviewComponent } from './channel/channel-preview.component';
|
||||
import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -12,6 +13,10 @@ const routes: Routes = [
|
||||
path: 'channel/:short_id',
|
||||
component: ChannelPreviewComponent,
|
||||
},
|
||||
{
|
||||
path: 'nodes/isp/:isp',
|
||||
component: NodesPerISPPreview,
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: ''
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { WebsocketService } from 'src/app/services/websocket.service';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightning-wrapper',
|
||||
|
||||
@@ -30,6 +30,7 @@ import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-ca
|
||||
import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
|
||||
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
|
||||
import { GroupComponent } from './group/group.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -58,6 +59,7 @@ import { NodeChannels } from '../lightning/nodes-channels/node-channels.componen
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
GroupComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import { GroupComponent } from './group/group.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -34,6 +35,10 @@ const routes: Routes = [
|
||||
path: 'nodes/isp/:isp',
|
||||
component: NodesPerISP,
|
||||
},
|
||||
{
|
||||
path: 'group/the-mempool-open-source-project',
|
||||
component: GroupComponent,
|
||||
},
|
||||
{
|
||||
path: 'nodes/rankings',
|
||||
component: NodesRankingsDashboard,
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Observable } from 'rxjs';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { StorageService } from 'src/app/services/storage.service';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
|
||||
|
||||
@@ -43,6 +43,13 @@
|
||||
|
||||
<ng-template #loadingReward>
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.capacity">Capacity</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.nodes">Nodes</h5>
|
||||
<div class="card-text">
|
||||
@@ -57,12 +64,5 @@
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.average-channels">Average Channel</h5>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
<div class="skeleton-loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -1,5 +1,7 @@
|
||||
<div class="box preview-box" *ngIf="(node$ | async) as node">
|
||||
<h2 class="preview-header" i18n="lightning.node">lightning node</h2>
|
||||
<app-preview-title>
|
||||
<span i18n="lightning.node">lightning node</span>
|
||||
</app-preview-title>
|
||||
<div class="row d-flex justify-content-between full-width-row">
|
||||
<h1 class="title"></h1>
|
||||
<div class="title-wrapper">
|
||||
@@ -54,7 +56,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md map-col">
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" [placeholder]="true" [hasLocation]="!!node.as_number" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" [placeholder]="true" [hasLocation]="!!node.as_number" [disableSpinner]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
|
||||
@@ -42,7 +42,7 @@ export class NodePreviewComponent implements OnInit {
|
||||
this.node$ = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.publicKey = params.get('public_key');
|
||||
this.publicKey = params.get('public_key');
|
||||
this.openGraphService.waitFor('node-map-' + this.publicKey);
|
||||
this.openGraphService.waitFor('node-data-' + this.publicKey);
|
||||
return this.lightningApiService.getNode$(params.get('public_key'));
|
||||
|
||||
@@ -44,11 +44,14 @@
|
||||
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="node.geolocation">
|
||||
<tr>
|
||||
<td i18n="location" class="text-truncate">Location</td>
|
||||
<td>
|
||||
<td *ngIf="node.geolocation">
|
||||
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
|
||||
</td>
|
||||
<td *ngIf="!node.geolocation">
|
||||
<span i18n="unknown">Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -75,13 +78,16 @@
|
||||
<div [ngStyle]="{'color': node.color}">{{ node.color }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr *ngIf="node.country">
|
||||
<tr>
|
||||
<td i18n="isp" class="text-truncate label">ISP</td>
|
||||
<td>
|
||||
<td *ngIf="node.as_number">
|
||||
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/isp' | relativeUrl, node.as_number]">
|
||||
{{ node.as_organization }} [ASN {{node.as_number}}]
|
||||
</a>
|
||||
</td>
|
||||
<td *ngIf="!node.as_number">
|
||||
<span class="badge badge-success" placement="bottom" i18n="tor">Exclusively on Tor</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -2,9 +2,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node',
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||
</div>
|
||||
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading">
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading && !disableSpinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Observable, switchMap, tap, zip } from 'rxjs';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { EChartsOption, registerMap } from 'echarts';
|
||||
import 'echarts-gl';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-channels-map',
|
||||
@@ -23,6 +23,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
@Input() fitContainer = false;
|
||||
@Input() hasLocation = true;
|
||||
@Input() placeholder = false;
|
||||
@Input() disableSpinner = false;
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
|
||||
channelsObservable: Observable<any>;
|
||||
|
||||
@@ -3,10 +3,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, O
|
||||
import { Router } from '@angular/router';
|
||||
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
|
||||
import { Observable, share, switchMap, tap } from 'rxjs';
|
||||
import { lerpColor } from 'src/app/shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { lerpColor } from '../../shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<div class="full-container">
|
||||
<div class="full-container" [class]="widget ? 'widget' : ''" [class.fit-container]="fitContainer">
|
||||
|
||||
<div class="card-header">
|
||||
<div *ngIf="!widget" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-world-map">Lightning nodes world map</span>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
<div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,22 @@
|
||||
padding-bottom: 100px;
|
||||
};
|
||||
}
|
||||
.full-container.widget {
|
||||
min-height: 240px;
|
||||
height: 240px;
|
||||
padding: 0px;
|
||||
}
|
||||
.full-container.fit-container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
min-height: 100px;
|
||||
|
||||
.chart {
|
||||
padding: 0;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
@@ -38,3 +54,6 @@
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
.chart.widget {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { Observable, tap, zip } from 'rxjs';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, Output, EventEmitter, LOCALE_ID, NgZone, OnDestroy, OnInit, OnChanges } from '@angular/core';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { Observable, BehaviorSubject, switchMap, tap, combineLatest } from 'rxjs';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { EChartsOption, registerMap } from 'echarts';
|
||||
import { lerpColor } from 'src/app/shared/graphs.utils';
|
||||
import { lerpColor } from '../../shared/graphs.utils';
|
||||
import { Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-map',
|
||||
@@ -17,7 +17,14 @@ import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||
styleUrls: ['./nodes-map.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesMap implements OnInit {
|
||||
export class NodesMap implements OnInit, OnChanges {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() nodes: any[] | undefined = undefined;
|
||||
@Input() type: 'none' | 'isp' | 'country' = 'none';
|
||||
@Input() fitContainer = false;
|
||||
@Output() readyEvent = new EventEmitter();
|
||||
inputNodes$: BehaviorSubject<any>;
|
||||
nodes$: Observable<any>;
|
||||
observable$: Observable<any>;
|
||||
|
||||
chartInstance = undefined;
|
||||
@@ -39,17 +46,62 @@ export class NodesMap implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning nodes world map`);
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`Lightning nodes world map`);
|
||||
}
|
||||
|
||||
this.observable$ = zip(
|
||||
if (!this.inputNodes$) {
|
||||
this.inputNodes$ = new BehaviorSubject(this.nodes);
|
||||
}
|
||||
|
||||
this.nodes$ = this.inputNodes$.pipe(
|
||||
switchMap((nodes) => nodes ? [nodes] : this.apiService.getWorldNodes$())
|
||||
);
|
||||
|
||||
this.observable$ = combineLatest(
|
||||
this.assetsService.getWorldMapJson$,
|
||||
this.apiService.getWorldNodes$()
|
||||
this.nodes$
|
||||
).pipe(tap((data) => {
|
||||
registerMap('world', data[0]);
|
||||
|
||||
let maxLiquidity = data[1].maxLiquidity;
|
||||
let inputNodes: any[] = data[1].nodes;
|
||||
let mapCenter: number[] = [0, 5];
|
||||
if (this.type === 'country') {
|
||||
mapCenter = [0, 0];
|
||||
} else if (this.type === 'isp') {
|
||||
mapCenter = [0, 10];
|
||||
}
|
||||
|
||||
let mapZoom = 1.3;
|
||||
if (!inputNodes) {
|
||||
inputNodes = [];
|
||||
for (const node of data[1]) {
|
||||
if (this.type === 'country') {
|
||||
mapCenter[0] += node.longitude;
|
||||
mapCenter[1] += node.latitude;
|
||||
}
|
||||
inputNodes.push([
|
||||
node.longitude,
|
||||
node.latitude,
|
||||
node.public_key,
|
||||
node.alias,
|
||||
node.capacity,
|
||||
node.channels,
|
||||
node.country,
|
||||
node.iso_code,
|
||||
]);
|
||||
maxLiquidity = Math.max(maxLiquidity ?? 0, node.capacity);
|
||||
}
|
||||
if (this.type === 'country') {
|
||||
mapCenter[0] /= data[1].length;
|
||||
mapCenter[1] /= data[1].length;
|
||||
mapZoom = 6;
|
||||
}
|
||||
}
|
||||
|
||||
const nodes: any[] = [];
|
||||
console.log(data[1].nodes[0]);
|
||||
for (const node of data[1].nodes) {
|
||||
for (const node of inputNodes) {
|
||||
// We add a bit of noise so nodes at the same location are not all
|
||||
// on top of each other
|
||||
const random = Math.random() * 2 * Math.PI;
|
||||
@@ -66,11 +118,22 @@ export class NodesMap implements OnInit {
|
||||
]);
|
||||
}
|
||||
|
||||
this.prepareChartOptions(nodes, data[1].maxLiquidity);
|
||||
maxLiquidity = Math.max(1, maxLiquidity);
|
||||
this.prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom);
|
||||
}));
|
||||
}
|
||||
|
||||
prepareChartOptions(nodes, maxLiquidity) {
|
||||
ngOnChanges(changes): void {
|
||||
if (changes.nodes) {
|
||||
if (!this.inputNodes$) {
|
||||
this.inputNodes$ = new BehaviorSubject(changes.nodes.currentValue);
|
||||
} else {
|
||||
this.inputNodes$.next(changes.nodes.currentValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom) {
|
||||
let title: object;
|
||||
if (nodes.length === 0) {
|
||||
title = {
|
||||
@@ -91,8 +154,8 @@ export class NodesMap implements OnInit {
|
||||
geo: {
|
||||
animation: false,
|
||||
silent: true,
|
||||
center: [0, 5],
|
||||
zoom: 1.3,
|
||||
center: mapCenter,
|
||||
zoom: mapZoom,
|
||||
tooltip: {
|
||||
show: false
|
||||
},
|
||||
@@ -122,10 +185,13 @@ export class NodesMap implements OnInit {
|
||||
return 10 * Math.pow(params[2] / maxLiquidity, 0.2) + 3;
|
||||
},
|
||||
tooltip: {
|
||||
position: function(point, params, dom, rect, size) {
|
||||
return point;
|
||||
},
|
||||
trigger: 'item',
|
||||
show: true,
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
borderRadius: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
@@ -155,7 +221,6 @@ export class NodesMap implements OnInit {
|
||||
borderColor: 'black',
|
||||
borderWidth: 0,
|
||||
},
|
||||
blendMode: 'lighter',
|
||||
zlevel: 2,
|
||||
},
|
||||
]
|
||||
@@ -182,4 +247,8 @@ export class NodesMap implements OnInit {
|
||||
this.chartInstance.resize();
|
||||
});
|
||||
}
|
||||
|
||||
onChartFinished(e) {
|
||||
this.readyEvent.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { StorageService } from 'src/app/services/storage.service';
|
||||
import { MiningService } from 'src/app/services/mining.service';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { MiningService } from '../../services/mining.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-networks-chart',
|
||||
|
||||
@@ -2,14 +2,14 @@ import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { map, Observable, share, tap } from 'rxjs';
|
||||
import { chartColors } from 'src/app/app.constants';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||
import { chartColors } from '../../app.constants';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-per-country-chart',
|
||||
|
||||
@@ -1,9 +1,58 @@
|
||||
<div class="container-xl full-height" style="min-height: 335px">
|
||||
<h1 class="float-left" i18n="lightning.nodes-in-country">
|
||||
<h1 i18n="lightning.nodes-in-country">
|
||||
<span>Lightning nodes in {{ country?.name }}</span>
|
||||
<span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
|
||||
</h1>
|
||||
|
||||
<div class="box">
|
||||
<div class="row" *ngIf="nodes$ | async as countryNodes">
|
||||
<div class="col-12 col-md-6">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="lightning.node-count">Nodes</td>
|
||||
<td>{{ countryNodes.nodes.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.liquidity">Liquidity</td>
|
||||
<td>
|
||||
<app-amount *ngIf="countryNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="countryNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
{{ countryNodes.sumLiquidity | amountShortener: 1 }}
|
||||
<span class="sats" i18n="shared.sats">sats</span>
|
||||
</ng-template>
|
||||
<span class="d-none d-md-inline-block"> </span>
|
||||
<span class="d-block d-md-none"></span>
|
||||
<app-fiat [value]="countryNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.channels">Channels</td>
|
||||
<td>{{ countryNodes.sumChannels }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.isp-count">ISP Count</td>
|
||||
<td>{{ countryNodes.ispCount }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.top-isp">Top ISP</td>
|
||||
<td class="text-truncate">
|
||||
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/isp' | relativeUrl, countryNodes.topIsp.id]">
|
||||
{{ countryNodes.topIsp.name }} [ASN {{ countryNodes.topIsp.id }}]
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 p-3 p-md-0 pr-md-3">
|
||||
<div style="background-color: #181b2d">
|
||||
<app-nodes-map [widget]="true" [nodes]="countryNodes.nodes" type="country"></app-nodes-map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
|
||||
@@ -15,9 +64,8 @@
|
||||
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
<th class="city text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
|
||||
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
|
||||
<tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
|
||||
<tbody *ngIf="nodes$ | async as countryNodes; else skeleton">
|
||||
<tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey">
|
||||
<td class="alias text-left text-truncate">
|
||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
</td>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
import { map, Observable, share } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-per-country',
|
||||
@@ -32,6 +32,8 @@ export class NodesPerCountry implements OnInit {
|
||||
this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
|
||||
.pipe(
|
||||
map(response => {
|
||||
this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
|
||||
|
||||
this.country = {
|
||||
name: response.country.en,
|
||||
flag: getFlagEmoji(this.route.snapshot.params.country)
|
||||
@@ -45,14 +47,50 @@ export class NodesPerCountry implements OnInit {
|
||||
iso: response.nodes[i].iso_code,
|
||||
};
|
||||
}
|
||||
|
||||
this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
|
||||
return response.nodes;
|
||||
})
|
||||
|
||||
const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
|
||||
const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
|
||||
const isps = {};
|
||||
const topIsp = {
|
||||
count: 0,
|
||||
id: '',
|
||||
name: '',
|
||||
};
|
||||
for (const node of response.nodes) {
|
||||
if (!node.isp) {
|
||||
continue;
|
||||
}
|
||||
if (!isps[node.isp]) {
|
||||
isps[node.isp] = {
|
||||
count: 0,
|
||||
asns: [],
|
||||
};
|
||||
}
|
||||
if (isps[node.isp].asns.indexOf(node.as_number) === -1) {
|
||||
isps[node.isp].asns.push(node.as_number);
|
||||
}
|
||||
isps[node.isp].count++;
|
||||
|
||||
if (isps[node.isp].count > topIsp.count) {
|
||||
topIsp.count = isps[node.isp].count;
|
||||
topIsp.id = isps[node.isp].asns.join(',');
|
||||
topIsp.name = node.isp;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: response.nodes,
|
||||
sumLiquidity: sumLiquidity,
|
||||
sumChannels: sumChannels,
|
||||
topIsp: topIsp,
|
||||
ispCount: Object.keys(isps).length
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
trackByPublicKey(index: number, node: any) {
|
||||
trackByPublicKey(index: number, node: any): string {
|
||||
return node.public_key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block">Unknown capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="lightning.unknown-capacity-desc"
|
||||
ngbTooltip="How much liquidity is running on nodes which ISP was not identifiable" placement="bottom">
|
||||
<app-amount [satoshis]="stats.unknownCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title d-inline-block">Tor capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5>
|
||||
<p class="card-text" i18n-ngbTooltip="lightning.tor-capacity-desc"
|
||||
ngbTooltip="How much liquidity is running on nodes advertising only Tor addresses" placement="bottom">
|
||||
<app-amount [satoshis]="stats.torCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
@@ -80,19 +80,19 @@
|
||||
<ng-template #loadingReward>
|
||||
<div class="pool-distribution">
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.tagged-isp">Tagged ISPs</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet capacity</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.tagged-capacity">Tagged capacity</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item">
|
||||
<h5 class="card-title" i18n="lightning.tagged-nodes">Tagged nodes</h5>
|
||||
<h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5>
|
||||
<p class="card-text">
|
||||
<span class="skeleton-loader skeleton-loader-big"></span>
|
||||
</p>
|
||||
|
||||
@@ -2,14 +2,14 @@ import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone, Input
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { combineLatest, map, Observable, share, startWith, Subject, switchMap, tap } from 'rxjs';
|
||||
import { chartColors } from 'src/app/app.constants';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { chartColors } from '../../app.constants';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-per-isp-chart',
|
||||
@@ -134,9 +134,6 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
data.push({
|
||||
itemStyle: {
|
||||
color: isp[0] === null ? '#7D4698' : undefined,
|
||||
},
|
||||
value: this.sortBy === 'capacity' ? isp[7] : isp[6],
|
||||
name: isp[1].replace('&', '') + (isMobile() || this.widget ? `` : ` (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)`),
|
||||
label: {
|
||||
@@ -206,7 +203,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
color: chartColors.slice(3),
|
||||
color: chartColors.filter((color) => color != '#5E35B1'), // Remove color that looks like Tor
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
textStyle: {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<div class="box preview-box" *ngIf="(nodes$ | async) as ispNodes">
|
||||
<app-preview-title>
|
||||
<span i18n="lightning.node">lightning ISP</span>
|
||||
</app-preview-title>
|
||||
<div class="row d-flex justify-content-between full-width-row">
|
||||
<div class="title-wrapper">
|
||||
<h1 class="title">{{ isp?.name }}</h1>
|
||||
<a class="subtitle" [routerLink]="['/lightning/nodes/isp/' | relativeUrl, isp?.id]">
|
||||
ASN {{ isp?.id }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="logo-wrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
<col span="1" width="250px">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="lightning.node-count">Nodes</td>
|
||||
<td>{{ ispNodes.nodes.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.liquidity">Liquidity</td>
|
||||
<td>
|
||||
<app-amount *ngIf="ispNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="ispNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
<app-sats [satoshis]="ispNodes.sumLiquidity" digitsInfo="1.0-2"></app-sats>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.channels">Channels</td>
|
||||
<td>{{ ispNodes.sumChannels }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.top-country">Top country</td>
|
||||
<td class="text-truncate">
|
||||
<span class="">{{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.top-node">Top node</td>
|
||||
<td class="text-truncate">
|
||||
{{ ispNodes.nodes[0].alias }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md map-col">
|
||||
<app-nodes-map [widget]="true" [nodes]="ispNodes.nodes" type="isp" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
<span i18n="error.general-loading-data">Error loading data.</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,31 @@
|
||||
.table {
|
||||
font-size: 32px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.map-col {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
width: 470px;
|
||||
height: 360px;
|
||||
min-width: 470px;
|
||||
min-height: 360px;
|
||||
max-height: 360px;
|
||||
padding: 0;
|
||||
background: #181b2d;
|
||||
overflow: hidden;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.full-width-row {
|
||||
padding-left: 15px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
::ng-deep .symbol {
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { catchError, map, switchMap, Observable, share, of } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-per-isp-preview',
|
||||
templateUrl: './nodes-per-isp-preview.component.html',
|
||||
styleUrls: ['./nodes-per-isp-preview.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesPerISPPreview implements OnInit {
|
||||
nodes$: Observable<any>;
|
||||
isp: {name: string, id: number};
|
||||
id: string;
|
||||
error: Error;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
private openGraphService: OpenGraphService,
|
||||
private route: ActivatedRoute,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.nodes$ = this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
this.id = params.get('isp');
|
||||
this.isp = null;
|
||||
this.openGraphService.waitFor('isp-map-' + this.id);
|
||||
this.openGraphService.waitFor('isp-data-' + this.id);
|
||||
return this.apiService.getNodeForISP$(params.get('isp'));
|
||||
}),
|
||||
map(response => {
|
||||
this.isp = {
|
||||
name: response.isp,
|
||||
id: this.route.snapshot.params.isp.split(',').join(', ')
|
||||
};
|
||||
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
||||
|
||||
for (const i in response.nodes) {
|
||||
response.nodes[i].geolocation = <GeolocationData>{
|
||||
country: response.nodes[i].country?.en,
|
||||
city: response.nodes[i].city?.en,
|
||||
subdivision: response.nodes[i].subdivision?.en,
|
||||
iso: response.nodes[i].iso_code,
|
||||
};
|
||||
}
|
||||
|
||||
const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
|
||||
const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
|
||||
const countries = {};
|
||||
const topCountry = {
|
||||
count: 0,
|
||||
country: '',
|
||||
iso: '',
|
||||
flag: '',
|
||||
};
|
||||
for (const node of response.nodes) {
|
||||
if (!node.geolocation.iso) {
|
||||
continue;
|
||||
}
|
||||
countries[node.geolocation.iso] = countries[node.geolocation.iso] ?? 0 + 1;
|
||||
if (countries[node.geolocation.iso] > topCountry.count) {
|
||||
topCountry.count = countries[node.geolocation.iso];
|
||||
topCountry.country = node.geolocation.country;
|
||||
topCountry.iso = node.geolocation.iso;
|
||||
}
|
||||
}
|
||||
topCountry.flag = getFlagEmoji(topCountry.iso);
|
||||
|
||||
this.openGraphService.waitOver('isp-data-' + this.id);
|
||||
|
||||
return {
|
||||
nodes: response.nodes,
|
||||
sumLiquidity: sumLiquidity,
|
||||
sumChannels: sumChannels,
|
||||
topCountry: topCountry,
|
||||
};
|
||||
}),
|
||||
catchError(err => {
|
||||
this.error = err;
|
||||
this.openGraphService.fail('isp-map-' + this.id);
|
||||
this.openGraphService.fail('isp-data-' + this.id);
|
||||
return of({
|
||||
nodes: [],
|
||||
sumLiquidity: 0,
|
||||
sumChannels: 0,
|
||||
topCountry: {},
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onMapReady() {
|
||||
this.openGraphService.waitOver('isp-map-' + this.id);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,54 @@
|
||||
<div class="container-xl full-height" style="min-height: 335px">
|
||||
<h1 class="float-left" i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }} [AS {{isp?.id}}]</h1>
|
||||
<h1 i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }}</h1>
|
||||
|
||||
<div class="box">
|
||||
<div class="row" *ngIf="nodes$ | async as ispNodes">
|
||||
<div class="col-12 col-md-6">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="lightning.asn">ASN</td>
|
||||
<td>{{ isp?.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.node-count">Nodes</td>
|
||||
<td>{{ ispNodes.nodes.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.liquidity">Liquidity</td>
|
||||
<td>
|
||||
<app-amount *ngIf="ispNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="ispNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
{{ ispNodes.sumLiquidity | amountShortener: 1 }}
|
||||
<span class="sats" i18n="shared.sats">sats</span>
|
||||
</ng-template>
|
||||
<span class="d-none d-md-inline-block"> </span>
|
||||
<span class="d-block d-md-none"></span>
|
||||
<app-fiat [value]="ispNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.channels">Channels</td>
|
||||
<td>{{ ispNodes.sumChannels }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.top-country">Top country</td>
|
||||
<td class="text-truncate">
|
||||
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/country' | relativeUrl, ispNodes.topCountry.iso]">
|
||||
<span class="">{{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }}</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 p-3 p-md-0 pr-md-3">
|
||||
<div style="background-color: #181b2d">
|
||||
<app-nodes-map [widget]="true" [nodes]="ispNodes.nodes" type="isp"></app-nodes-map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
@@ -12,9 +61,8 @@
|
||||
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||
<th class="city text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
|
||||
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
|
||||
<tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
|
||||
<tbody *ngIf="nodes$ | async as ispNodes; else skeleton">
|
||||
<tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey">
|
||||
<td class="alias text-left text-truncate">
|
||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
</td>
|
||||
|
||||
@@ -59,4 +59,4 @@
|
||||
@media (max-width: 576px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
import { map, Observable, share } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-per-isp',
|
||||
@@ -33,7 +34,7 @@ export class NodesPerISP implements OnInit {
|
||||
map(response => {
|
||||
this.isp = {
|
||||
name: response.isp,
|
||||
id: this.route.snapshot.params.isp
|
||||
id: this.route.snapshot.params.isp.split(',').join(', ')
|
||||
};
|
||||
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
||||
|
||||
@@ -46,12 +47,40 @@ export class NodesPerISP implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
return response.nodes;
|
||||
})
|
||||
const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
|
||||
const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
|
||||
const countries = {};
|
||||
const topCountry = {
|
||||
count: 0,
|
||||
country: '',
|
||||
iso: '',
|
||||
flag: '',
|
||||
};
|
||||
for (const node of response.nodes) {
|
||||
if (!node.geolocation.iso) {
|
||||
continue;
|
||||
}
|
||||
countries[node.geolocation.iso] = countries[node.geolocation.iso] ?? 0 + 1;
|
||||
if (countries[node.geolocation.iso] > topCountry.count) {
|
||||
topCountry.count = countries[node.geolocation.iso];
|
||||
topCountry.country = node.geolocation.country;
|
||||
topCountry.iso = node.geolocation.iso;
|
||||
}
|
||||
}
|
||||
topCountry.flag = getFlagEmoji(topCountry.iso);
|
||||
|
||||
return {
|
||||
nodes: response.nodes,
|
||||
sumLiquidity: sumLiquidity,
|
||||
sumChannels: sumChannels,
|
||||
topCountry: topCountry,
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
trackByPublicKey(index: number, node: any) {
|
||||
trackByPublicKey(index: number, node: any): string {
|
||||
return node.public_key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { IOldestNodes } from '../../../interfaces/node-api.interface';
|
||||
import { LightningApiService } from '../../lightning-api.service';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
import { INodesRanking, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { isMobile } from '../../../shared/common.utils';
|
||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||
import { LightningApiService } from '../../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { isMobile } from '../../../shared/common.utils';
|
||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||
import { LightningApiService } from '../../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { INodesRanking } from '../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -2,15 +2,15 @@ import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angul
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { StorageService } from 'src/app/services/storage.service';
|
||||
import { MiningService } from 'src/app/services/mining.service';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { StorageService } from '../../services/storage.service';
|
||||
import { MiningService } from '../../services/mining.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from 'src/app/shared/common.utils';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightning-statistics-chart',
|
||||
|
||||
Reference in New Issue
Block a user