Merge branch 'master' into hunicus/move-on-in-it
This commit is contained in:
@@ -66,9 +66,7 @@
|
||||
</div>
|
||||
|
||||
<ng-template [ngIf]="error">
|
||||
<div class="text-center">
|
||||
<app-http-error [error]="error">
|
||||
<span i18n="error.general-loading-data">Error loading data.</span>
|
||||
<br><br>
|
||||
<i>{{ error.status }}: {{ error.error }}</i>
|
||||
</div>
|
||||
</app-http-error>
|
||||
</ng-template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesP
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LightningApiService {
|
||||
private apiBaseUrl: string; // base URL is protocol, hostname, and port
|
||||
private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
|
||||
@@ -16,11 +17,12 @@ export class LightningApiService {
|
||||
private httpClient: HttpClient,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.apiBaseUrl = ''; // use relative URL by default
|
||||
if (!stateService.isBrowser) { // except when inside AU SSR process
|
||||
this.apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT;
|
||||
}
|
||||
this.apiBasePath = ''; // assume mainnet by default
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) {
|
||||
network = '';
|
||||
}
|
||||
this.apiBasePath = network ? '/' + network : '';
|
||||
});
|
||||
}
|
||||
@@ -66,15 +68,15 @@ export class LightningApiService {
|
||||
}
|
||||
|
||||
getNode$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||
}
|
||||
|
||||
getNodeGroup$(name: string): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + 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);
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
|
||||
}
|
||||
|
||||
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
||||
@@ -84,57 +86,57 @@ export class LightningApiService {
|
||||
.set('status', status)
|
||||
;
|
||||
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' });
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' });
|
||||
}
|
||||
|
||||
getLatestStatistics$(): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics/latest');
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/statistics/latest');
|
||||
}
|
||||
|
||||
listNodeStats$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||
}
|
||||
|
||||
getNodeFeeHistogram$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
|
||||
}
|
||||
|
||||
getNodesRanking$(): Observable<INodesRanking> {
|
||||
return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
|
||||
return this.httpClient.get<INodesRanking>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings');
|
||||
}
|
||||
|
||||
listChannelStats$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/channels/' + publicKey + '/statistics');
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/channels/' + publicKey + '/statistics');
|
||||
}
|
||||
|
||||
listStatistics$(interval: string | undefined): Observable<any> {
|
||||
return this.httpClient.get<any>(
|
||||
this.apiBasePath + '/api/v1/lightning/statistics' +
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/statistics' +
|
||||
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
|
||||
);
|
||||
}
|
||||
|
||||
getTopNodesByCapacity$(): Observable<ITopNodesPerCapacity[]> {
|
||||
return this.httpClient.get<ITopNodesPerCapacity[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/liquidity'
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/liquidity'
|
||||
);
|
||||
}
|
||||
|
||||
getTopNodesByChannels$(): Observable<ITopNodesPerChannels[]> {
|
||||
return this.httpClient.get<ITopNodesPerChannels[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/connectivity'
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/connectivity'
|
||||
);
|
||||
}
|
||||
|
||||
getPenaltyClosedChannels$(): Observable<IChannel[]> {
|
||||
return this.httpClient.get<IChannel[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/penalties'
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/penalties'
|
||||
);
|
||||
}
|
||||
|
||||
getOldestNodes$(): Observable<IOldestNodes[]> {
|
||||
return this.httpClient.get<IOldestNodes[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,9 +34,11 @@
|
||||
|
||||
<!-- ISP pie chart -->
|
||||
<div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card graph-card">
|
||||
<div class="card">
|
||||
<div class="card-body pl-2 pr-2">
|
||||
<app-nodes-per-isp-chart [widget]="true"></app-nodes-per-isp-chart>
|
||||
<div class="mempool-graph">
|
||||
<app-nodes-per-isp-chart [height]="graphHeight" [widget]="true"></app-nodes-per-isp-chart>
|
||||
</div>
|
||||
<div style="margin-top: 5px"><a [attr.data-cy]="'pool-distribution-view-more'" [routerLink]="['/graphs/lightning/nodes-per-isp' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,11 +46,13 @@
|
||||
|
||||
<!-- Network history -->
|
||||
<div class="col">
|
||||
<div class="card graph-card">
|
||||
<div class="card">
|
||||
<div class="card-body pl-2 pr-2 pt-1">
|
||||
<h5 class="card-title mt-3" i18n="lightning.network-history">Lightning Network History</h5>
|
||||
<app-lightning-statistics-chart [widget]=true></app-lightning-statistics-chart>
|
||||
<app-nodes-networks-chart [widget]=true></app-nodes-networks-chart>
|
||||
<div class="mempool-graph">
|
||||
<h5 class="card-title mt-3" i18n="lightning.network-history">Lightning Network History</h5>
|
||||
<app-lightning-statistics-chart [height]="(graphHeight / 1.7)" [widget]=true></app-lightning-statistics-chart>
|
||||
<app-nodes-networks-chart [height]="(graphHeight / 1.7)" [widget]=true></app-nodes-networks-chart>
|
||||
</div>
|
||||
<div><a [routerLink]="['/graphs/lightning/nodes-networks' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-mempool-graph {
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
.mempool-graph, .fixed-mempool-graph {
|
||||
@media (min-width: 768px) {
|
||||
height: 345px;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
height: 439px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1rem;
|
||||
color: #4a68b9;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnInit } from '@angular/core';
|
||||
import { Observable, merge } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { OpenGraphService } from '../../services/opengraph.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@@ -16,22 +17,45 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||
statistics$: Observable<INodesStatistics>;
|
||||
nodesRanking$: Observable<INodesRanking>;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
graphHeight: number = 300;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private seoService: SeoService,
|
||||
private ogService: OpenGraphService,
|
||||
private stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.onResize();
|
||||
|
||||
this.seoService.setTitle($localize`:@@142e923d3b04186ac6ba23387265d22a2fa404e0:Lightning Explorer`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.dashboard:Get stats on the Lightning network (aggregate capacity, connectivity, etc), Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).`);
|
||||
this.ogService.setManualOgImage('lightning.jpg');
|
||||
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
|
||||
if (!this.stateService.isBrowser) {
|
||||
merge(this.nodesRanking$, this.statistics$).subscribe(() => {
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
}
|
||||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize(): void {
|
||||
if (window.innerWidth >= 992) {
|
||||
this.graphHeight = 340;
|
||||
} else if (window.innerWidth >= 768) {
|
||||
this.graphHeight = 245;
|
||||
} else {
|
||||
this.graphHeight = 210;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-node
|
||||
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';
|
||||
import { NodeOwnerComponent } from './node-owner/node-owner.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -67,7 +66,6 @@ import { NodeOwnerComponent } from './node-owner/node-owner.component';
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
GroupComponent,
|
||||
NodeOwnerComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@@ -105,7 +103,6 @@ import { NodeOwnerComponent } from './node-owner/node-owner.component';
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
NodeOwnerComponent,
|
||||
],
|
||||
providers: [
|
||||
LightningApiService,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="full-container">
|
||||
<h2 i18n="lightning.node-fee-distribution">Fee distribution</h2>
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
.full-container {
|
||||
position: relative;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
min-height: 100%;
|
||||
}
|
||||
min-height: 450px;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-fee-chart',
|
||||
@@ -33,6 +34,7 @@ export class NodeFeeChartComponent implements OnInit {
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
public stateService: StateService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<div *ngIf="stateService.env.OFFICIAL_MEMPOOL_SPACE === true">
|
||||
|
||||
<div *ngIf="{ value: (nodeOwner$ | async) } as nodeOwner">
|
||||
|
||||
<div *ngIf="nodeOwner.value && nodeOwner.value.sns_id">
|
||||
<a target="_blank" [href]="'https://twitter.com/' + nodeOwner.value.username">
|
||||
<img class="profile-photo" [src]="'data:' + nodeOwner.value.image_mime + ';base64,' + nodeOwner.value.image">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div *ngIf="nodeOwner.value === false">
|
||||
<a [href]="'/login/lnnode?type=signup&pubkey=' + publicKey + '&alias=' + alias" class="btn btn-primary btn-sm">Claim</a>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
.profile-photo {
|
||||
width: 31px;
|
||||
height: 31px;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-owner',
|
||||
templateUrl: './node-owner.component.html',
|
||||
styleUrls: ['./node-owner.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodeOwnerComponent{
|
||||
@Input() publicKey: string = '';
|
||||
@Input() alias: string = '';
|
||||
@Input() nodeOwner$: Observable<any>;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="full-container">
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { StorageService } from '../../services/storage.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-statistics-chart',
|
||||
@@ -48,6 +49,7 @@ export class NodeStatisticsChartComponent implements OnInit {
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
private storageService: StorageService,
|
||||
public stateService: StateService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<div class="title-container mb-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h1 class="mb-0 text-truncate">{{ node.alias }}</h1>
|
||||
<!-- <app-node-owner [nodeOwner$]="nodeOwner$" [publicKey]="node.public_key" [alias]="node.alias" class="claim-btn"></app-node-owner> -->
|
||||
</div>
|
||||
<span class="tx-link justify-content-between align-items-center">
|
||||
<span class="node-id">
|
||||
@@ -13,7 +12,6 @@
|
||||
<app-clipboard [text]="node.public_key"></app-clipboard>
|
||||
</app-truncate>
|
||||
</span>
|
||||
<!-- <app-node-owner [nodeOwner$]="nodeOwner$" [publicKey]="node.public_key" [alias]="node.alias" class="claim-btn-mobile"></app-node-owner> -->
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, ChangeDetectorRef } from '@angular/core';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { Observable, of, EMPTY } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { catchError, map, switchMap, tap, share } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
@@ -8,6 +8,7 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
import { GeolocationData } from '../../shared/components/geolocation/geolocation.component';
|
||||
import { ILiquidityAd, parseLiquidityAdHex } from './liquidity-ad';
|
||||
import { haversineDistance, kmToMiles } from '../../../app/shared/common.utils';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
|
||||
interface CustomRecord {
|
||||
type: string;
|
||||
@@ -38,11 +39,11 @@ export class NodeComponent implements OnInit {
|
||||
tlvRecords: CustomRecord[];
|
||||
avgChannelDistance$: Observable<number | null>;
|
||||
showFeatures = false;
|
||||
nodeOwner$: Observable<any>;
|
||||
kmToMiles = kmToMiles;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private lightningApiService: LightningApiService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private seoService: SeoService,
|
||||
@@ -151,24 +152,6 @@ export class NodeComponent implements OnInit {
|
||||
return null;
|
||||
})
|
||||
) as Observable<number | null>;
|
||||
|
||||
this.nodeOwner$ = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
return this.apiService.getNodeOwner$(params.get('public_key')).pipe(
|
||||
switchMap((response) => {
|
||||
if (response.status === 204) {
|
||||
return of(false);
|
||||
}
|
||||
return of(response.body);
|
||||
}),
|
||||
catchError(() => {
|
||||
return of(false);
|
||||
})
|
||||
)
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
|
||||
toggleShowDetails(): void {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<div class="map-wrapper" [class]="style" *ngIf="style !== 'graph'">
|
||||
<ng-container *ngIf="channelsObservable | async">
|
||||
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart" [class]="style" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||
</div>
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading && !disableSpinner">
|
||||
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="(!stateService.isBrowser || isLoading) && !disableSpinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -21,8 +22,10 @@
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="channelsObservable | async" class="chart-graph" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
<ng-container *ngIf="channelsObservable | async">
|
||||
<div class="chart-graph" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
min-height: 400px;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
min-height: 100%;
|
||||
}
|
||||
.full-container.widget {
|
||||
height: 250px;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { delay, Observable, switchMap, tap, zip } from 'rxjs';
|
||||
import { delay, Observable, of, switchMap, tap, zip } from 'rxjs';
|
||||
import { AssetsService } from '../../services/assets.service';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { EChartsOption, echarts } from '../../graphs/echarts';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
import { lerpColor } from '../../shared/graphs.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-channels-map',
|
||||
@@ -45,11 +48,12 @@ export class NodesChannelsMap implements OnInit {
|
||||
constructor(
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private assetsService: AssetsService,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -86,10 +90,12 @@ export class NodesChannelsMap implements OnInit {
|
||||
return zip(
|
||||
this.assetsService.getWorldMapJson$,
|
||||
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined, this.style) : [''],
|
||||
[params.get('public_key') ?? undefined]
|
||||
[params.get('public_key') ?? undefined],
|
||||
this.style === 'widget' ? of(undefined) : this.apiService.getWorldNodes$(),
|
||||
).pipe(tap((data) => {
|
||||
echarts.registerMap('world', data[0]);
|
||||
|
||||
let maxLiquidity = data[3]?.maxLiquidity;
|
||||
const channelsLoc = [];
|
||||
const nodes = [];
|
||||
const nodesPubkeys = {};
|
||||
@@ -197,13 +203,24 @@ export class NodesChannelsMap implements OnInit {
|
||||
this.zoom = -0.05 * distance + 8;
|
||||
}
|
||||
|
||||
this.prepareChartOptions(nodes, channelsLoc);
|
||||
if (data[3]) {
|
||||
for (const node of nodes) {
|
||||
const foundNode = data[3].nodes.find((n) => n[2] === node[3]);
|
||||
if (foundNode) {
|
||||
node.push(foundNode[4], foundNode[5], foundNode[6]?.en, foundNode[7]);
|
||||
maxLiquidity = Math.max(maxLiquidity ?? 0, foundNode[4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxLiquidity = Math.max(1, maxLiquidity);
|
||||
this.prepareChartOptions(nodes, channelsLoc, maxLiquidity);
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(nodes, channels) {
|
||||
prepareChartOptions(nodes, channels, maxLiquidity) {
|
||||
let title: object;
|
||||
if (channels.length === 0) {
|
||||
if (!this.placeholder) {
|
||||
@@ -267,7 +284,12 @@ export class NodesChannelsMap implements OnInit {
|
||||
data: nodes,
|
||||
coordinateSystem: 'geo',
|
||||
geoIndex: 0,
|
||||
symbolSize: this.nodeSize,
|
||||
symbolSize: (params) => {
|
||||
if (maxLiquidity) {
|
||||
return 10 * Math.pow(params[5] / maxLiquidity, 0.2) + 3;
|
||||
}
|
||||
return this.nodeSize;
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
@@ -281,11 +303,25 @@ export class NodesChannelsMap implements OnInit {
|
||||
formatter: (value) => {
|
||||
const data = value.data;
|
||||
const alias = data[4].length > 0 ? data[4] : data[3].slice(0, 20);
|
||||
return `<b style="color: white">${alias}</b>`;
|
||||
}
|
||||
const liquidity = data[5] >= 100000000 ?
|
||||
`${this.amountShortenerPipe.transform(data[5] / 100000000)} BTC` :
|
||||
`${this.amountShortenerPipe.transform(data[5], 2)} sats`;
|
||||
|
||||
return `
|
||||
<b style="color: white">${alias}</b><br>
|
||||
${liquidity}<br>` +
|
||||
$localize`:@@205c1b86ac1cc419c4d0cca51fdde418c4ffdc20:${data[6]}:INTERPOLATION: channels` + `<br>
|
||||
${getFlagEmoji(data[8])} ${data[7]}
|
||||
`;
|
||||
},
|
||||
},
|
||||
itemStyle: {
|
||||
color: 'white',
|
||||
color: (params) => {
|
||||
if (!maxLiquidity) {
|
||||
return 'white';
|
||||
}
|
||||
return `${lerpColor('#1E88E5', '#D81B60', Math.pow(params.data[5] / maxLiquidity, 0.2))}`;
|
||||
},
|
||||
opacity: 1,
|
||||
borderColor: 'black',
|
||||
borderWidth: 0,
|
||||
@@ -361,8 +397,6 @@ export class NodesChannelsMap implements OnInit {
|
||||
}
|
||||
|
||||
chartOptions.series[0].itemStyle.borderWidth = nodeBorder;
|
||||
chartOptions.series[0].symbolSize += e.zoom > 1 ? speed * 15 : -speed * 15;
|
||||
chartOptions.series[0].symbolSize = Math.max(4, Math.min(7, chartOptions.series[0].symbolSize));
|
||||
|
||||
chartOptions.series[1].lineStyle.opacity += e.zoom > 1 ? speed : -speed;
|
||||
chartOptions.series[1].lineStyle.width += e.zoom > 1 ? speed : -speed;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
|
||||
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
|
||||
<div echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
||||
<div class="node-channels-container">
|
||||
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
|
||||
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
|
||||
<div *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!stateService.isBrowser || isLoading" class="text-center loading-spinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isLoading" class="text-center loading-spinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
.node-channels-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
min-height: 455px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export class NodeChannels implements OnChanges {
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private zone: NgZone,
|
||||
private router: Router,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
|
||||
@@ -7,8 +7,13 @@
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
<ng-container *ngIf="observable$ | async">
|
||||
<div class="chart" [class]="widget ? 'widget' : ''" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="text-center loading-spinner" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -63,3 +63,13 @@
|
||||
.chart.widget {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
@media (max-width: 767.98px) {
|
||||
top: 550px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
inputNodes$: BehaviorSubject<any>;
|
||||
nodes$: Observable<any>;
|
||||
observable$: Observable<any>;
|
||||
isLoading: boolean = true;
|
||||
|
||||
chartInstance = undefined;
|
||||
chartOptions: EChartsOption = {};
|
||||
@@ -37,7 +38,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private assetsService: AssetsService,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
@@ -88,7 +89,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
node.public_key,
|
||||
node.alias,
|
||||
node.capacity,
|
||||
node.active_channel_count,
|
||||
node.channels,
|
||||
node.country,
|
||||
node.iso_code,
|
||||
]);
|
||||
@@ -226,6 +227,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
},
|
||||
]
|
||||
};
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
@@ -235,6 +237,10 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('finished', () => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
if (e.data) {
|
||||
this.zone.run(() => {
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 145px;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { echarts, EChartsOption, LineSeriesOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
@@ -11,6 +11,7 @@ import { SeoService } from '../../services/seo.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-networks-chart',
|
||||
@@ -26,7 +27,8 @@ import { isMobile } from '../../shared/common.utils';
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesNetworksChartComponent implements OnInit {
|
||||
export class NodesNetworksChartComponent implements OnInit, OnChanges {
|
||||
@Input() height: number = 150;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 45;
|
||||
@Input() widget = false;
|
||||
@@ -47,6 +49,9 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
|
||||
chartData: any;
|
||||
maxYAxis: number;
|
||||
|
||||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
@@ -54,6 +59,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
public stateService: StateService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
}
|
||||
@@ -71,44 +77,49 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.nodesNetworkObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.miningWindowPreference),
|
||||
switchMap((timespan) => {
|
||||
this.timespan = timespan;
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('lightningWindowPreference', timespan);
|
||||
}
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
|
||||
.pipe(
|
||||
tap((response:any) => {
|
||||
const data = response.body;
|
||||
const chartData = {
|
||||
tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]),
|
||||
clearnet_nodes: data.map(val => [val.added * 1000, val.clearnet_nodes]),
|
||||
unannounced_nodes: data.map(val => [val.added * 1000, val.unannounced_nodes]),
|
||||
clearnet_tor_nodes: data.map(val => [val.added * 1000, val.clearnet_tor_nodes]),
|
||||
};
|
||||
let maxYAxis = 0;
|
||||
for (const day of data) {
|
||||
maxYAxis = Math.max(maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes + day.clearnet_tor_nodes);
|
||||
}
|
||||
maxYAxis = Math.ceil(maxYAxis / 3000) * 3000;
|
||||
this.prepareChartOptions(chartData, maxYAxis);
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((response) => {
|
||||
return {
|
||||
days: parseInt(response.headers.get('x-total-count'), 10),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
this.nodesNetworkObservable$ = this.radioGroupForm.get('dateSpan').valueChanges.pipe(
|
||||
startWith(this.miningWindowPreference),
|
||||
switchMap((timespan) => {
|
||||
this.timespan = timespan;
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('lightningWindowPreference', timespan);
|
||||
}
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
|
||||
.pipe(
|
||||
tap((response:any) => {
|
||||
const data = response.body;
|
||||
this.chartData = {
|
||||
tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]),
|
||||
clearnet_nodes: data.map(val => [val.added * 1000, val.clearnet_nodes]),
|
||||
unannounced_nodes: data.map(val => [val.added * 1000, val.unannounced_nodes]),
|
||||
clearnet_tor_nodes: data.map(val => [val.added * 1000, val.clearnet_tor_nodes]),
|
||||
};
|
||||
this.maxYAxis = 0;
|
||||
for (const day of data) {
|
||||
this.maxYAxis = Math.max(this.maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes + day.clearnet_tor_nodes);
|
||||
}
|
||||
this.maxYAxis = Math.ceil(this.maxYAxis / 3000) * 3000;
|
||||
this.prepareChartOptions(this.chartData, this.maxYAxis);
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((response) => {
|
||||
return {
|
||||
days: parseInt(response.headers.get('x-total-count'), 10),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.height && this.chartData && this.maxYAxis != null) {
|
||||
this.prepareChartOptions(this.chartData, this.maxYAxis);
|
||||
}
|
||||
}
|
||||
|
||||
prepareChartOptions(data, maxYAxis): void {
|
||||
@@ -228,7 +239,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
title: title,
|
||||
animation: false,
|
||||
grid: {
|
||||
height: this.widget ? 90 : undefined,
|
||||
height: this.widget ? ((this.height || 120) - 60) : undefined,
|
||||
top: this.widget ? 20 : 40,
|
||||
bottom: this.widget ? 0 : 70,
|
||||
right: (isMobile() && this.widget) ? 35 : this.right,
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
<div class="container pb-lg-0">
|
||||
<div class="pb-lg-5">
|
||||
<div class="chart w-100" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart w-100" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export class NodesPerCountryChartComponent implements OnInit {
|
||||
private seoService: SeoService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private zone: NgZone,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private router: Router,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -64,8 +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 countryNodes; else skeleton">
|
||||
<tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey">
|
||||
<tbody *ngIf="nodesPagination$ | async as countryNodes; else skeleton">
|
||||
<tr *ngFor="let node of countryNodes; let i= index; trackBy: trackByPublicKey">
|
||||
<td class="alias text-left text-truncate">
|
||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
</td>
|
||||
@@ -116,5 +116,10 @@
|
||||
</ng-template>
|
||||
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="nodes$ | async as countryNodes" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="countryNodes.nodes.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="pageSize" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,14 +22,14 @@
|
||||
|
||||
.timestamp-first {
|
||||
width: 20%;
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 1060px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
.timestamp-update {
|
||||
width: 16%;
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 1060px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
.city {
|
||||
max-width: 150px;
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 675px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map, Observable, share } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
@@ -15,15 +15,22 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation
|
||||
export class NodesPerCountry implements OnInit {
|
||||
nodes$: Observable<any>;
|
||||
country: {name: string, flag: string};
|
||||
nodesPagination$: Observable<any>;
|
||||
startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
page = 1;
|
||||
pageSize = 15;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
isLoading = true;
|
||||
|
||||
skeletonLines: number[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
for (let i = 0; i < this.pageSize; ++i) {
|
||||
this.skeletonLines.push(i);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +38,7 @@ export class NodesPerCountry implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
|
||||
.pipe(
|
||||
tap(() => this.isLoading = true),
|
||||
map(response => {
|
||||
this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.nodes-country:Explore all the Lightning nodes hosted in ${response.country.en} and see an overview of each node's capacity, number of open channels, and more.`);
|
||||
@@ -87,11 +95,24 @@ export class NodesPerCountry implements OnInit {
|
||||
ispCount: Object.keys(isps).length
|
||||
};
|
||||
}),
|
||||
tap(() => {
|
||||
this.isLoading = false
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe(
|
||||
map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize))
|
||||
);
|
||||
}
|
||||
|
||||
trackByPublicKey(index: number, node: any): string {
|
||||
return node.public_key;
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.startingIndexSubject.next((page - 1) * this.pageSize);
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +39,10 @@
|
||||
</div>
|
||||
|
||||
<div *ngIf="!indexingInProgress else indexing" [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-md-end toggle" *ngIf="!widget">
|
||||
<app-toggle [textLeft]="'Sort by nodes'" [textRight]="'capacity'" [checked]="true" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
|
||||
</div>
|
||||
@@ -74,6 +70,9 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingReward>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pi
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesPerISPChartComponent implements OnInit {
|
||||
@Input() height: number = 300;
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
isLoading = true;
|
||||
@@ -43,7 +44,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -61,8 +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 ispNodes; else skeleton">
|
||||
<tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey">
|
||||
<tbody *ngIf="nodesPagination$ | async as ispNodes; else skeleton">
|
||||
<tr *ngFor="let node of ispNodes; let i= index; trackBy: trackByPublicKey">
|
||||
<td class="alias text-left text-truncate">
|
||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
</td>
|
||||
@@ -113,5 +113,10 @@
|
||||
</ng-template>
|
||||
|
||||
</table>
|
||||
|
||||
<ngb-pagination *ngIf="nodes$ | async as ispNodes" class="pagination-container float-right mt-2" [class]="isLoading ? 'disabled' : ''"
|
||||
[collectionSize]="ispNodes.nodes.length" [rotate]="true" [maxSize]="maxSize" [pageSize]="pageSize" [(page)]="page"
|
||||
(pageChange)="pageChange(page)" [boundaryLinks]="true" [ellipses]="false">
|
||||
</ngb-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
.timestamp-first {
|
||||
width: 20%;
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 1060px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@
|
||||
.timestamp-update {
|
||||
width: 16%;
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 1060px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@
|
||||
.city {
|
||||
max-width: 150px;
|
||||
|
||||
@media (max-width: 576px) {
|
||||
@media (max-width: 675px) {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { map, Observable, share } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { getFlagEmoji } from '../../shared/common.utils';
|
||||
@@ -15,6 +15,12 @@ import { GeolocationData } from '../../shared/components/geolocation/geolocation
|
||||
export class NodesPerISP implements OnInit {
|
||||
nodes$: Observable<any>;
|
||||
isp: {name: string, id: number};
|
||||
nodesPagination$: Observable<any>;
|
||||
startingIndexSubject: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
page = 1;
|
||||
pageSize = 15;
|
||||
maxSize = window.innerWidth <= 767.98 ? 3 : 5;
|
||||
isLoading = true;
|
||||
|
||||
skeletonLines: number[] = [];
|
||||
|
||||
@@ -23,7 +29,7 @@ export class NodesPerISP implements OnInit {
|
||||
private seoService: SeoService,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
for (let i = 0; i < this.pageSize; ++i) {
|
||||
this.skeletonLines.push(i);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +37,7 @@ export class NodesPerISP implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp)
|
||||
.pipe(
|
||||
tap(() => this.isLoading = true),
|
||||
map(response => {
|
||||
this.isp = {
|
||||
name: response.isp,
|
||||
@@ -77,11 +84,21 @@ export class NodesPerISP implements OnInit {
|
||||
topCountry: topCountry,
|
||||
};
|
||||
}),
|
||||
tap(() => this.isLoading = false),
|
||||
share()
|
||||
);
|
||||
|
||||
this.nodesPagination$ = combineLatest([this.nodes$, this.startingIndexSubject]).pipe(
|
||||
map(([response, startingIndex]) => response.nodes.slice(startingIndex, startingIndex + this.pageSize))
|
||||
);
|
||||
}
|
||||
|
||||
trackByPublicKey(index: number, node: any): string {
|
||||
return node.public_key;
|
||||
}
|
||||
|
||||
pageChange(page: number): void {
|
||||
this.startingIndexSubject.next((page - 1) * this.pageSize);
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
}
|
||||
.chart-widget {
|
||||
width: 100%;
|
||||
height: 145px;
|
||||
}
|
||||
|
||||
.pool-distribution {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, combineLatest, fromEvent } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { formatNumber } from '@angular/common';
|
||||
@@ -11,6 +11,7 @@ import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightning-statistics-chart',
|
||||
@@ -25,7 +26,8 @@ import { isMobile } from '../../shared/common.utils';
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class LightningStatisticsChartComponent implements OnInit {
|
||||
export class LightningStatisticsChartComponent implements OnInit, OnChanges {
|
||||
@Input() height: number = 150;
|
||||
@Input() right: number | string = 45;
|
||||
@Input() left: number | string = 45;
|
||||
@Input() widget = false;
|
||||
@@ -37,6 +39,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
chartData: any;
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
@@ -53,6 +56,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
public stateService: StateService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
}
|
||||
@@ -70,36 +74,42 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: this.miningWindowPreference });
|
||||
this.radioGroupForm.controls.dateSpan.setValue(this.miningWindowPreference);
|
||||
|
||||
this.capacityObservable$ = this.radioGroupForm.get('dateSpan').valueChanges
|
||||
.pipe(
|
||||
startWith(this.miningWindowPreference),
|
||||
switchMap((timespan) => {
|
||||
this.timespan = timespan;
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('lightningWindowPreference', timespan);
|
||||
}
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
|
||||
.pipe(
|
||||
tap((response:any) => {
|
||||
const data = response.body;
|
||||
this.prepareChartOptions({
|
||||
channel_count: data.map(val => [val.added * 1000, val.channel_count]),
|
||||
capacity: data.map(val => [val.added * 1000, val.total_capacity]),
|
||||
});
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((response) => {
|
||||
return {
|
||||
days: parseInt(response.headers.get('x-total-count'), 10),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
this.capacityObservable$ = this.radioGroupForm.get('dateSpan').valueChanges.pipe(
|
||||
startWith(this.miningWindowPreference),
|
||||
switchMap((timespan) => {
|
||||
this.timespan = timespan;
|
||||
if (!this.widget && !firstRun) {
|
||||
this.storageService.setValue('lightningWindowPreference', timespan);
|
||||
}
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
|
||||
.pipe(
|
||||
tap((response:any) => {
|
||||
const data = response.body;
|
||||
this.chartData = {
|
||||
channel_count: data.map(val => [val.added * 1000, val.channel_count]),
|
||||
capacity: data.map(val => [val.added * 1000, val.total_capacity]),
|
||||
};
|
||||
this.prepareChartOptions(this.chartData);
|
||||
this.isLoading = false;
|
||||
}),
|
||||
map((response) => {
|
||||
return {
|
||||
days: parseInt(response.headers.get('x-total-count'), 10),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.height && this.chartData) {
|
||||
this.prepareChartOptions(this.chartData);
|
||||
}
|
||||
}
|
||||
|
||||
prepareChartOptions(data): void {
|
||||
@@ -138,7 +148,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
]),
|
||||
],
|
||||
grid: {
|
||||
height: this.widget ? 90 : undefined,
|
||||
height: this.widget ? ((this.height || 120) - 60) : undefined,
|
||||
top: this.widget ? 20 : 40,
|
||||
bottom: this.widget ? 0 : 70,
|
||||
right: (isMobile() && this.widget) ? 35 : this.right,
|
||||
|
||||
Reference in New Issue
Block a user