Merge branch 'master' into hunicus/move-on-in-it
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="lightning.alias">Alias</th>
|
||||
<th class="nodedetails text-left"> </th>
|
||||
<th class="status text-left" i18n="status">Status</th>
|
||||
<th class="status text-left" i18n="transaction.status|Transaction Status">Status</th>
|
||||
<th class="feerate text-left" *ngIf="status !== 'closed'" i18n="transaction.fee-rate|Transaction fee rate">Fee rate</th>
|
||||
<th class="feerate text-left" *ngIf="status === 'closed'" i18n="channels.closing_date">Closing date</th>
|
||||
<th class="liquidity text-right" i18n="lightning.capacity">Capacity</th>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { INodesStatistics } from '../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channels-statistics',
|
||||
@@ -8,7 +9,7 @@ import { Observable } from 'rxjs';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ChannelsStatisticsComponent implements OnInit {
|
||||
@Input() statistics$: Observable<any>;
|
||||
@Input() statistics$: Observable<INodesStatistics>;
|
||||
mode: string = 'avg';
|
||||
|
||||
constructor() { }
|
||||
|
||||
@@ -57,7 +57,7 @@ export class GroupPreviewComponent implements OnInit {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.lightningApiService.getNodGroupNodes$(this.groupId);
|
||||
return this.lightningApiService.getNodeGroup$(this.groupId);
|
||||
}),
|
||||
map((nodes) => {
|
||||
for (const node of nodes) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td i18n>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>
|
||||
@@ -70,7 +70,7 @@
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="lightning.alias">Alias</th>
|
||||
<th class="text-left">Connect</th>
|
||||
<th class="text-left" i18n="lightning.connect-to-node|Connect">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">
|
||||
|
||||
@@ -41,7 +41,7 @@ export class GroupComponent implements OnInit {
|
||||
this.seoService.setTitle(`Mempool.space Lightning Nodes`);
|
||||
this.seoService.setDescription(`See all Lightning nodes run by mempool.space -- these are the nodes that provide the data on the mempool.space Lightning dashboard.`);
|
||||
|
||||
this.nodes$ = this.lightningApiService.getNodGroupNodes$('mempool.space')
|
||||
this.nodes$ = this.lightningApiService.getNodeGroup$('mempool.space')
|
||||
.pipe(
|
||||
map((nodes) => {
|
||||
for (const node of nodes) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
|
||||
import { StateService } from '../services/state.service';
|
||||
import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
|
||||
|
||||
@@ -9,6 +9,8 @@ import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesP
|
||||
})
|
||||
export class LightningApiService {
|
||||
private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
@@ -23,11 +25,51 @@ export class LightningApiService {
|
||||
});
|
||||
}
|
||||
|
||||
private generateCacheKey(functionName: string, params: any[]): string {
|
||||
return functionName + JSON.stringify(params);
|
||||
}
|
||||
|
||||
// delete expired cache entries
|
||||
private cleanExpiredCache(): void {
|
||||
this.requestCache.forEach((value, key) => {
|
||||
if (value.expiry < Date.now()) {
|
||||
this.requestCache.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cachedRequest<T, F extends (...args: any[]) => Observable<T>>(
|
||||
apiFunction: F,
|
||||
expireAfter: number, // in ms
|
||||
...params: Parameters<F>
|
||||
): Observable<T> {
|
||||
this.cleanExpiredCache();
|
||||
|
||||
const cacheKey = this.generateCacheKey(apiFunction.name, params);
|
||||
if (!this.requestCache.has(cacheKey)) {
|
||||
const subject = new BehaviorSubject<T | null>(null);
|
||||
this.requestCache.set(cacheKey, { subject, expiry: Date.now() + expireAfter });
|
||||
|
||||
apiFunction.bind(this)(...params).pipe(
|
||||
tap(data => {
|
||||
subject.next(data as T);
|
||||
}),
|
||||
catchError((error) => {
|
||||
subject.error(error);
|
||||
return of(null);
|
||||
}),
|
||||
shareReplay(1),
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
return this.requestCache.get(cacheKey).subject.asObservable().pipe(filter(val => val !== null), take(1));
|
||||
}
|
||||
|
||||
getNode$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||
}
|
||||
|
||||
getNodGroupNodes$(name: string): Observable<any[]> {
|
||||
getNodeGroup$(name: string): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [statistics$]="statistics$" [widget]="true"></app-top-nodes-per-capacity>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,7 +77,7 @@
|
||||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [statistics$]="statistics$" [widget]="true"></app-top-nodes-per-channels>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { INodesRanking } from '../../interfaces/node-api.interface';
|
||||
import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
@@ -13,7 +13,7 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||
statistics$: Observable<any>;
|
||||
statistics$: Observable<INodesStatistics>;
|
||||
nodesRanking$: Observable<INodesRanking>;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
|
||||
@@ -25,7 +25,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
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) and Lightning nodes (channels, liquidity, etc) and Lightning channels (status, fees, etc).`);
|
||||
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.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { formatNumber } from '@angular/common';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { INodesStatistics } from '../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-statistics',
|
||||
@@ -8,7 +9,7 @@ import { Observable } from 'rxjs';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodeStatisticsComponent implements OnInit {
|
||||
@Input() statistics$: Observable<any>;
|
||||
@Input() statistics$: Observable<INodesStatistics>;
|
||||
|
||||
constructor() { }
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<tr *ngIf="!node.city && !node.country">
|
||||
<td i18n="lightning.location">Location</td>
|
||||
<td>
|
||||
<span>unknown</span>
|
||||
<span i18n="unknown">Unknown</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
</div>
|
||||
|
||||
<ng-template #featurebits let-bits="bits">
|
||||
<td i18n="lightning.features" class="text-truncate label">Features</td>
|
||||
<td i18n="transaction.features|Transaction features" class="text-truncate label">Features</td>
|
||||
<td class="d-flex justify-content-between">
|
||||
<span class="text-truncate w-90">{{ bits }}</span>
|
||||
<button type="button" class="btn btn-outline-info btn-xs" (click)="toggleFeatures()" i18n="transaction.details|Transaction Details">Details</button>
|
||||
@@ -133,11 +133,11 @@
|
||||
<h5>Raw bits</h5>
|
||||
<span class="text-wrap w-100"><small>{{ node.featuresBits }}</small></span>
|
||||
</div>
|
||||
<h5>Decoded</h5>
|
||||
<h5 i18n="lightning.decoded|Decoded">Decoded</h5>
|
||||
<table class="table table-borderless table-striped table-fixed">
|
||||
<thead>
|
||||
<th style="width: 13%">Bit</th>
|
||||
<th>Name</th>
|
||||
<th i18n="lightning.as-name">Name</th>
|
||||
<th style="width: 25%; text-align: right">Required</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<div class="map-wrapper" [class]="style">
|
||||
<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 *ngIf="style === 'graph'" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</span>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div class="chart" [class]="style" 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="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="full-container-graph" *ngIf="style === 'graph'">
|
||||
|
||||
<div class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning Nodes Channels World Map</span>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -143,3 +143,55 @@
|
||||
text-align: center;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.full-container-graph {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 225px);
|
||||
min-height: 400px;
|
||||
@media (min-width: 992px) {
|
||||
height: calc(100vh - 150px);
|
||||
}
|
||||
}
|
||||
.full-container-graph.widget {
|
||||
min-height: 240px;
|
||||
height: 240px;
|
||||
padding: 0px;
|
||||
}
|
||||
.full-container-graph.fit-container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
min-height: 100px;
|
||||
|
||||
.chart {
|
||||
padding: 0;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-graph {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding-top: 30px;
|
||||
padding-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
@media (max-width: 992px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 829px) {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
padding-bottom: 25px;
|
||||
}
|
||||
@media (max-width: 629px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
@media (max-width: 567px) {
|
||||
padding-bottom: 55px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ 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, registerMap } from 'echarts';
|
||||
import 'echarts-gl';
|
||||
import { EChartsOption, echarts } from '../../graphs/echarts';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
|
||||
@Component({
|
||||
@@ -66,6 +65,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
}
|
||||
|
||||
if (this.style === 'graph') {
|
||||
this.center = [0, 5];
|
||||
this.seoService.setTitle($localize`Lightning Nodes Channels World Map`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.node-map:See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details.`);
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export class NodesChannelsMap implements OnInit {
|
||||
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined, this.style) : [''],
|
||||
[params.get('public_key') ?? undefined]
|
||||
).pipe(tap((data) => {
|
||||
registerMap('world', data[0]);
|
||||
echarts.registerMap('world', data[0]);
|
||||
|
||||
const channelsLoc = [];
|
||||
const nodes = [];
|
||||
@@ -239,7 +239,6 @@ export class NodesChannelsMap implements OnInit {
|
||||
title: title ?? undefined,
|
||||
tooltip: {},
|
||||
geo: {
|
||||
top: 75,
|
||||
animation: false,
|
||||
silent: true,
|
||||
center: this.center,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { formatNumber } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ECharts, EChartsOption, TreemapSeriesOption } from 'echarts';
|
||||
import { EChartsOption, TreemapSeriesOption } from '../../graphs/echarts';
|
||||
import { Observable, share, switchMap, tap } from 'rxjs';
|
||||
import { lerpColor } from '../../shared/graphs.utils';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
@@ -18,7 +18,7 @@ import { StateService } from '../../services/state.service';
|
||||
export class NodeChannels implements OnChanges {
|
||||
@Input() publicKey: string;
|
||||
|
||||
chartInstance: ECharts;
|
||||
chartInstance: any;
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
@@ -129,7 +129,7 @@ export class NodeChannels implements OnChanges {
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec: ECharts): void {
|
||||
onChartInit(ec: any): void {
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { EChartsOption, echarts } from '../../graphs/echarts';
|
||||
import { lerpColor } from '../../shared/graphs.utils';
|
||||
import { Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
@@ -63,7 +63,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
this.assetsService.getWorldMapJson$,
|
||||
this.nodes$
|
||||
).pipe(tap((data) => {
|
||||
registerMap('world', data[0]);
|
||||
echarts.registerMap('world', data[0]);
|
||||
|
||||
let maxLiquidity = data[1].maxLiquidity;
|
||||
let inputNodes: any[] = data[1].nodes;
|
||||
@@ -88,7 +88,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
node.public_key,
|
||||
node.alias,
|
||||
node.capacity,
|
||||
node.channels,
|
||||
node.active_channel_count,
|
||||
node.country,
|
||||
node.iso_code,
|
||||
]);
|
||||
@@ -114,7 +114,7 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
node[3], // Alias
|
||||
node[2], // Public key
|
||||
node[5], // Channels
|
||||
node[6].en, // Country
|
||||
node[6]?.en, // Country
|
||||
node[7], // ISO Code
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption, graphic, LineSeriesOption} from 'echarts';
|
||||
import { echarts, EChartsOption, LineSeriesOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { formatNumber } from '@angular/common';
|
||||
@@ -82,9 +82,9 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.listStatistics$(timespan)
|
||||
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
tap((response:any) => {
|
||||
const data = response.body;
|
||||
const chartData = {
|
||||
tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]),
|
||||
@@ -152,7 +152,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
color: new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#D81B60' },
|
||||
{ offset: 1, color: '#D81B60AA' },
|
||||
]),
|
||||
@@ -174,7 +174,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
color: new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#be7d4c' },
|
||||
{ offset: 1, color: '#be7d4cAA' },
|
||||
]),
|
||||
@@ -195,7 +195,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
color: new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#FFB300' },
|
||||
{ offset: 1, color: '#FFB300AA' },
|
||||
]),
|
||||
@@ -216,7 +216,7 @@ export class NodesNetworksChartComponent implements OnInit {
|
||||
opacity: 0.5,
|
||||
},
|
||||
stack: 'Total',
|
||||
color: new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
color: new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#7D4698' },
|
||||
{ offset: 1, color: '#7D4698AA' },
|
||||
]),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { EChartsOption, PieSeriesOption } from '../../graphs/echarts';
|
||||
import { map, Observable, share, tap } from 'rxjs';
|
||||
import { chartColors } from '../../app.constants';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone, Input } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { EChartsOption, PieSeriesOption } from '../../graphs/echarts';
|
||||
import { combineLatest, map, Observable, share, startWith, Subject, switchMap, tap } from 'rxjs';
|
||||
import { chartColors } from '../../app.constants';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<app-top-nodes-per-capacity [nodes$]="null" [widget]="false" *ngIf="type === 'capacity'">
|
||||
<app-top-nodes-per-capacity [nodes$]="null" [statistics$]="statistics$" [widget]="false" *ngIf="type === 'capacity'">
|
||||
</app-top-nodes-per-capacity>
|
||||
|
||||
<app-top-nodes-per-channels [nodes$]="null" [widget]="false" *ngIf="type === 'channels'">
|
||||
<app-top-nodes-per-channels [nodes$]="null" [statistics$]="statistics$" [widget]="false" *ngIf="type === 'channels'">
|
||||
</app-top-nodes-per-channels>
|
||||
|
||||
<app-oldest-nodes [widget]="false" *ngIf="type === 'oldest'"></app-oldest-nodes>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
import { INodesStatistics } from '../../interfaces/node-api.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-ranking',
|
||||
@@ -9,10 +13,15 @@ import { ActivatedRoute } from '@angular/router';
|
||||
})
|
||||
export class NodesRanking implements OnInit {
|
||||
type: string;
|
||||
statistics$: Observable<INodesStatistics>;
|
||||
|
||||
constructor(private route: ActivatedRoute) {}
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private lightningApiService: LightningApiService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
this.route.data.subscribe(data => {
|
||||
this.type = data.type;
|
||||
});
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
|
||||
<th *ngIf="!widget" class="d-none d-md-table-cell text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="topNodesPerCapacity$ | async as nodes">
|
||||
<tr *ngFor="let node of nodes;">
|
||||
<tbody *ngIf="topNodesPerCapacity$ | async as data">
|
||||
<tr *ngFor="let node of data.nodes;">
|
||||
<td class="pool text-left">
|
||||
<div class="tooltip-custom d-block w-100">
|
||||
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
|
||||
@@ -27,12 +27,14 @@
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
<span class="capacity-ratio"> ({{ (node?.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%)</span>
|
||||
</td>
|
||||
<td class="d-table-cell fiat text-right" [ngClass]="{'widget': widget}">
|
||||
<app-fiat [value]="node.capacity"></app-fiat>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="d-none d-md-table-cell text-right">
|
||||
{{ node.channels | number }}
|
||||
<span class="capacity-ratio"> ({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%)</span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="d-none d-md-table-cell text-right">
|
||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>
|
||||
|
||||
@@ -41,6 +41,11 @@ tr, td, th {
|
||||
}
|
||||
}
|
||||
|
||||
.capacity-ratio {
|
||||
font-size: 12px;
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
.fiat {
|
||||
width: 15%;
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { INodesRanking, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface';
|
||||
import { combineLatest, map, Observable } from 'rxjs';
|
||||
import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||
@@ -14,9 +14,10 @@ import { LightningApiService } from '../../lightning-api.service';
|
||||
})
|
||||
export class TopNodesPerCapacity implements OnInit {
|
||||
@Input() nodes$: Observable<INodesRanking>;
|
||||
@Input() statistics$: Observable<INodesStatistics>;
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
|
||||
topNodesPerCapacity$: Observable<{ nodes: ITopNodesPerCapacity[]; statistics: { totalCapacity: number; totalChannels?: number; } }>;
|
||||
skeletonRows: number[] = [];
|
||||
currency$: Observable<string>;
|
||||
|
||||
@@ -39,8 +40,12 @@ export class TopNodesPerCapacity implements OnInit {
|
||||
}
|
||||
|
||||
if (this.widget === false) {
|
||||
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe(
|
||||
map((ranking) => {
|
||||
this.topNodesPerCapacity$ = combineLatest([
|
||||
this.apiService.getTopNodesByCapacity$(),
|
||||
this.statistics$
|
||||
])
|
||||
.pipe(
|
||||
map(([ranking, statistics]) => {
|
||||
for (const i in ranking) {
|
||||
ranking[i].geolocation = <GeolocationData>{
|
||||
country: ranking[i].country?.en,
|
||||
@@ -49,13 +54,28 @@ export class TopNodesPerCapacity implements OnInit {
|
||||
iso: ranking[i].iso_code,
|
||||
};
|
||||
}
|
||||
return ranking;
|
||||
return {
|
||||
nodes: ranking,
|
||||
statistics: {
|
||||
totalCapacity: statistics.latest.total_capacity,
|
||||
totalChannels: statistics.latest.channel_count,
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.topNodesPerCapacity$ = this.nodes$.pipe(
|
||||
map((ranking) => {
|
||||
return ranking.topByCapacity.slice(0, 6);
|
||||
this.topNodesPerCapacity$ = combineLatest([
|
||||
this.nodes$,
|
||||
this.statistics$
|
||||
])
|
||||
.pipe(
|
||||
map(([ranking, statistics]) => {
|
||||
return {
|
||||
nodes: ranking.topByCapacity.slice(0, 6),
|
||||
statistics: {
|
||||
totalCapacity: statistics.latest.total_capacity,
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
|
||||
<th class="geolocation d-table-cell text-right" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="topNodesPerChannels$ | async as nodes">
|
||||
<tr *ngFor="let node of nodes;">
|
||||
<tbody *ngIf="topNodesPerChannels$ | async as data">
|
||||
<tr *ngFor="let node of data.nodes;">
|
||||
<td class="pool text-left">
|
||||
<div class="tooltip-custom d-block w-100">
|
||||
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
|
||||
@@ -27,9 +27,11 @@
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ node.channels ? (node.channels | number) : '~' }}
|
||||
<span class="capacity-ratio"> ({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%)</span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="d-none d-md-table-cell capacity text-right">
|
||||
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
<span class="capacity-ratio"> ({{ (node.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%)</span>
|
||||
</td>
|
||||
<td *ngIf="!widget" class="fiat d-none d-md-table-cell text-right">
|
||||
<app-fiat [value]="node.capacity"></app-fiat>
|
||||
|
||||
@@ -44,6 +44,11 @@ tr, td, th {
|
||||
}
|
||||
}
|
||||
|
||||
.capacity-ratio {
|
||||
font-size: 12px;
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
.geolocation {
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
display: none !important;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
||||
import { combineLatest, map, Observable } from 'rxjs';
|
||||
import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||
@@ -14,12 +14,13 @@ import { LightningApiService } from '../../lightning-api.service';
|
||||
})
|
||||
export class TopNodesPerChannels implements OnInit {
|
||||
@Input() nodes$: Observable<INodesRanking>;
|
||||
@Input() statistics$: Observable<INodesStatistics>;
|
||||
@Input() widget: boolean = false;
|
||||
|
||||
topNodesPerChannels$: Observable<ITopNodesPerChannels[]>;
|
||||
topNodesPerChannels$: Observable<{ nodes: ITopNodesPerChannels[]; statistics: { totalChannels: number; totalCapacity?: number; } }>;
|
||||
skeletonRows: number[] = [];
|
||||
currency$: Observable<string>;
|
||||
|
||||
|
||||
constructor(
|
||||
private apiService: LightningApiService,
|
||||
private stateService: StateService,
|
||||
@@ -37,8 +38,12 @@ export class TopNodesPerChannels implements OnInit {
|
||||
this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.channels:See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.`);
|
||||
|
||||
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe(
|
||||
map((ranking) => {
|
||||
this.topNodesPerChannels$ = combineLatest([
|
||||
this.apiService.getTopNodesByChannels$(),
|
||||
this.statistics$
|
||||
])
|
||||
.pipe(
|
||||
map(([ranking, statistics]) => {
|
||||
for (const i in ranking) {
|
||||
ranking[i].geolocation = <GeolocationData>{
|
||||
country: ranking[i].country?.en,
|
||||
@@ -47,12 +52,22 @@ export class TopNodesPerChannels implements OnInit {
|
||||
iso: ranking[i].iso_code,
|
||||
};
|
||||
}
|
||||
return ranking;
|
||||
return {
|
||||
nodes: ranking,
|
||||
statistics: {
|
||||
totalChannels: statistics.latest.channel_count,
|
||||
totalCapacity: statistics.latest.total_capacity,
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.topNodesPerChannels$ = this.nodes$.pipe(
|
||||
map((ranking) => {
|
||||
this.topNodesPerChannels$ = combineLatest([
|
||||
this.nodes$,
|
||||
this.statistics$
|
||||
])
|
||||
.pipe(
|
||||
map(([ranking, statistics]) => {
|
||||
for (const i in ranking.topByChannels) {
|
||||
ranking.topByChannels[i].geolocation = <GeolocationData>{
|
||||
country: ranking.topByChannels[i].country?.en,
|
||||
@@ -61,7 +76,12 @@ export class TopNodesPerChannels implements OnInit {
|
||||
iso: ranking.topByChannels[i].iso_code,
|
||||
};
|
||||
}
|
||||
return ranking.topByChannels.slice(0, 6);
|
||||
return {
|
||||
nodes: ranking.topByChannels.slice(0, 6),
|
||||
statistics: {
|
||||
totalChannels: statistics.latest.channel_count,
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class NodesRankingsDashboard implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Top lightning nodes`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.rankings-dashboard:See top the Lightning network nodes ranked by liquidity, connectivity, and age.`);
|
||||
this.seoService.setDescription($localize`:@@meta.description.lightning.rankings-dashboard:See the top Lightning network nodes ranked by liquidity, connectivity, and age.`);
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
@@ -81,9 +81,9 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
firstRun = false;
|
||||
this.miningWindowPreference = timespan;
|
||||
this.isLoading = true;
|
||||
return this.lightningApiService.listStatistics$(timespan)
|
||||
return this.lightningApiService.cachedRequest(this.lightningApiService.listStatistics$, 250, timespan)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
tap((response:any) => {
|
||||
const data = response.body;
|
||||
this.prepareChartOptions({
|
||||
channel_count: data.map(val => [val.added * 1000, val.channel_count]),
|
||||
@@ -132,7 +132,7 @@ export class LightningStatisticsChartComponent implements OnInit {
|
||||
animation: false,
|
||||
color: [
|
||||
'#FFB300',
|
||||
new graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
new echarts.graphic.LinearGradient(0, 0.75, 0, 1, [
|
||||
{ offset: 0, color: '#D81B60' },
|
||||
{ offset: 1, color: '#D81B60AA' },
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user