mirror of
https://github.com/mempool/mempool.git
synced 2025-02-23 06:35:15 +01:00
Merge pull request #2315 from mempool/nykappa/feature/node-ranking
Refactor top nodes widgets, create related top 100 nodes pages
This commit is contained in:
commit
d2240532c1
18 changed files with 631 additions and 52 deletions
|
@ -2,6 +2,7 @@ import logger from '../../logger';
|
||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
import { ResultSetHeader } from 'mysql2';
|
import { ResultSetHeader } from 'mysql2';
|
||||||
import { ILightningApi } from '../lightning/lightning-api.interface';
|
import { ILightningApi } from '../lightning/lightning-api.interface';
|
||||||
|
import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces';
|
||||||
|
|
||||||
class NodesApi {
|
class NodesApi {
|
||||||
public async $getNode(public_key: string): Promise<any> {
|
public async $getNode(public_key: string): Promise<any> {
|
||||||
|
@ -112,20 +113,46 @@ class NodesApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getTopCapacityNodes(): Promise<any> {
|
public async $getTopCapacityNodes(full: boolean): Promise<ITopNodesPerCapacity[]> {
|
||||||
try {
|
try {
|
||||||
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
|
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
|
||||||
const latestDate = rows[0].maxAdded;
|
const latestDate = rows[0].maxAdded;
|
||||||
|
|
||||||
const query = `
|
let query: string;
|
||||||
SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels
|
if (full === false) {
|
||||||
|
query = `
|
||||||
|
SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
|
||||||
|
node_stats.capacity
|
||||||
FROM node_stats
|
FROM node_stats
|
||||||
JOIN nodes ON nodes.public_key = node_stats.public_key
|
JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||||
WHERE added = FROM_UNIXTIME(${latestDate})
|
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||||
ORDER BY capacity DESC
|
ORDER BY capacity DESC
|
||||||
LIMIT 10;
|
LIMIT 100
|
||||||
`;
|
`;
|
||||||
|
|
||||||
[rows] = await DB.query(query);
|
[rows] = await DB.query(query);
|
||||||
|
} else {
|
||||||
|
query = `
|
||||||
|
SELECT node_stats.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(node_stats.public_key, 1, 20), alias) as alias,
|
||||||
|
CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity,
|
||||||
|
CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||||
|
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
|
||||||
|
geo_names_city.names as city, geo_names_country.names as country
|
||||||
|
FROM node_stats
|
||||||
|
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||||
|
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||||
|
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||||
|
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||||
|
ORDER BY capacity DESC
|
||||||
|
LIMIT 100
|
||||||
|
`;
|
||||||
|
|
||||||
|
[rows] = await DB.query(query);
|
||||||
|
for (let i = 0; i < rows.length; ++i) {
|
||||||
|
rows[i].country = JSON.parse(rows[i].country);
|
||||||
|
rows[i].city = JSON.parse(rows[i].city);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -134,20 +161,46 @@ class NodesApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getTopChannelsNodes(): Promise<any> {
|
public async $getTopChannelsNodes(full: boolean): Promise<ITopNodesPerChannels[]> {
|
||||||
try {
|
try {
|
||||||
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
|
let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats');
|
||||||
const latestDate = rows[0].maxAdded;
|
const latestDate = rows[0].maxAdded;
|
||||||
|
|
||||||
const query = `
|
let query: string;
|
||||||
SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels
|
if (full === false) {
|
||||||
|
query = `
|
||||||
|
SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
|
||||||
|
node_stats.channels
|
||||||
FROM node_stats
|
FROM node_stats
|
||||||
JOIN nodes ON nodes.public_key = node_stats.public_key
|
JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||||
WHERE added = FROM_UNIXTIME(${latestDate})
|
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||||
ORDER BY channels DESC
|
ORDER BY channels DESC
|
||||||
LIMIT 10;
|
LIMIT 100;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
[rows] = await DB.query(query);
|
[rows] = await DB.query(query);
|
||||||
|
} else {
|
||||||
|
query = `
|
||||||
|
SELECT node_stats.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(node_stats.public_key, 1, 20), alias) as alias,
|
||||||
|
CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
|
||||||
|
CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity,
|
||||||
|
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
|
||||||
|
geo_names_city.names as city, geo_names_country.names as country
|
||||||
|
FROM node_stats
|
||||||
|
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
|
||||||
|
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
|
||||||
|
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||||
|
WHERE added = FROM_UNIXTIME(${latestDate})
|
||||||
|
ORDER BY channels DESC
|
||||||
|
LIMIT 100
|
||||||
|
`;
|
||||||
|
|
||||||
|
[rows] = await DB.query(query);
|
||||||
|
for (let i = 0; i < rows.length; ++i) {
|
||||||
|
rows[i].country = JSON.parse(rows[i].country);
|
||||||
|
rows[i].city = JSON.parse(rows[i].city);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import config from '../../config';
|
||||||
import { Application, Request, Response } from 'express';
|
import { Application, Request, Response } from 'express';
|
||||||
import nodesApi from './nodes.api';
|
import nodesApi from './nodes.api';
|
||||||
import DB from '../../database';
|
import DB from '../../database';
|
||||||
|
import { INodesRanking } from '../../mempool.interfaces';
|
||||||
|
|
||||||
class NodesRoutes {
|
class NodesRoutes {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
@ -10,10 +11,12 @@ class NodesRoutes {
|
||||||
app
|
app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes)
|
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings', this.$getNodesRanking)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/capacity', this.$getTopNodesByCapacity)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/rankings/channels', this.$getTopNodesByChannels)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
|
||||||
;
|
;
|
||||||
|
@ -56,11 +59,14 @@ class NodesRoutes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getTopNodes(req: Request, res: Response) {
|
private async $getNodesRanking(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const topCapacityNodes = await nodesApi.$getTopCapacityNodes();
|
const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false);
|
||||||
const topChannelsNodes = await nodesApi.$getTopChannelsNodes();
|
const topChannelsNodes = await nodesApi.$getTopChannelsNodes(false);
|
||||||
res.json({
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
res.json(<INodesRanking>{
|
||||||
topByCapacity: topCapacityNodes,
|
topByCapacity: topCapacityNodes,
|
||||||
topByChannels: topChannelsNodes,
|
topByChannels: topChannelsNodes,
|
||||||
});
|
});
|
||||||
|
@ -69,6 +75,30 @@ class NodesRoutes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async $getTopNodesByCapacity(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const topCapacityNodes = await nodesApi.$getTopCapacityNodes(true);
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
res.json(topCapacityNodes);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async $getTopNodesByChannels(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const topCapacityNodes = await nodesApi.$getTopChannelsNodes(true);
|
||||||
|
res.header('Pragma', 'public');
|
||||||
|
res.header('Cache-control', 'public');
|
||||||
|
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
|
||||||
|
res.json(topCapacityNodes);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async $getISPRanking(req: Request, res: Response): Promise<void> {
|
private async $getISPRanking(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const nodesPerAs = await nodesApi.$getNodesISPRanking();
|
const nodesPerAs = await nodesApi.$getNodesISPRanking();
|
||||||
|
|
|
@ -251,3 +251,30 @@ export interface RewardStats {
|
||||||
totalFee: number;
|
totalFee: number;
|
||||||
totalTx: number;
|
totalTx: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ITopNodesPerChannels {
|
||||||
|
publicKey: string,
|
||||||
|
alias: string,
|
||||||
|
channels?: number,
|
||||||
|
capacity: number,
|
||||||
|
firstSeen?: number,
|
||||||
|
updatedAt?: number,
|
||||||
|
city?: any,
|
||||||
|
country?: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITopNodesPerCapacity {
|
||||||
|
publicKey: string,
|
||||||
|
alias: string,
|
||||||
|
capacity: number,
|
||||||
|
channels?: number,
|
||||||
|
firstSeen?: number,
|
||||||
|
updatedAt?: number,
|
||||||
|
city?: any,
|
||||||
|
country?: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodesRanking {
|
||||||
|
topByCapacity: ITopNodesPerCapacity[];
|
||||||
|
topByChannels: ITopNodesPerChannels[];
|
||||||
|
}
|
|
@ -151,3 +151,30 @@ export interface RewardStats {
|
||||||
totalFee: number;
|
totalFee: number;
|
||||||
totalTx: number;
|
totalTx: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ITopNodesPerChannels {
|
||||||
|
publicKey: string,
|
||||||
|
alias: string,
|
||||||
|
channels?: number,
|
||||||
|
capacity: number,
|
||||||
|
firstSeen?: number,
|
||||||
|
updatedAt?: number,
|
||||||
|
city?: any,
|
||||||
|
country?: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITopNodesPerCapacity {
|
||||||
|
publicKey: string,
|
||||||
|
alias: string,
|
||||||
|
capacity: number,
|
||||||
|
channels?: number,
|
||||||
|
firstSeen?: number,
|
||||||
|
updatedAt?: number,
|
||||||
|
city?: any,
|
||||||
|
country?: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodesRanking {
|
||||||
|
topByCapacity: ITopNodesPerCapacity[];
|
||||||
|
topByChannels: ITopNodesPerChannels[];
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { StateService } from '../services/state.service';
|
import { StateService } from '../services/state.service';
|
||||||
|
import { INodesRanking, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
@ -48,8 +49,8 @@ export class LightningApiService {
|
||||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||||
}
|
}
|
||||||
|
|
||||||
listTopNodes$(): Observable<any> {
|
getNodesRanking$(): Observable<INodesRanking> {
|
||||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/top');
|
return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
|
||||||
}
|
}
|
||||||
|
|
||||||
listChannelStats$(publicKey: string): Observable<any> {
|
listChannelStats$(publicKey: string): Observable<any> {
|
||||||
|
@ -62,4 +63,16 @@ export class LightningApiService {
|
||||||
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
|
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTopNodesByCapacity$(): Observable<ITopNodesPerCapacity[]> {
|
||||||
|
return this.httpClient.get<ITopNodesPerCapacity[]>(
|
||||||
|
this.apiBasePath + '/api/v1/lightning/nodes/rankings/capacity'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTopNodesByChannels$(): Observable<ITopNodesPerChannels[]> {
|
||||||
|
return this.httpClient.get<ITopNodesPerChannels[]>(
|
||||||
|
this.apiBasePath + '/api/v1/lightning/nodes/rankings/channels'
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,9 +56,13 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Top Capacity Nodes</h5>
|
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-capacity' | relativeUrl]">
|
||||||
<app-nodes-list [nodes$]="nodesByCapacity$" [show]="'mobile-capacity'"></app-nodes-list>
|
<h5 class="card-title d-inline" i18n="lightning.top-capacity-nodes">Top capacity nodes</h5>
|
||||||
<!-- <div><a [routerLink]="['/lightning/nodes' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> -->
|
<span> </span>
|
||||||
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<h5 class="card-title"></h5>
|
||||||
|
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,9 +70,12 @@
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Most Connected Nodes</h5>
|
<a class="title-link" href="" [routerLink]="['/lightning/nodes/top-channels' | relativeUrl]">
|
||||||
<app-nodes-list [nodes$]="nodesByChannels$" [show]="'mobile-channels'"></app-nodes-list>
|
<h5 class="card-title d-inline" i18n="lightning.top-channels-nodes">Most connected nodes</h5>
|
||||||
<!-- <div><a [routerLink]="['/lightning/nodes' | relativeUrl]" i18n="dashboard.view-more">View more »</a></div> -->
|
<span> </span>
|
||||||
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map, share } from 'rxjs/operators';
|
import { share } from 'rxjs/operators';
|
||||||
|
import { INodesRanking } from 'src/app/interfaces/node-api.interface';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
|
|
||||||
|
@ -11,9 +12,8 @@ import { LightningApiService } from '../lightning-api.service';
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class LightningDashboardComponent implements OnInit {
|
export class LightningDashboardComponent implements OnInit {
|
||||||
nodesByCapacity$: Observable<any>;
|
|
||||||
nodesByChannels$: Observable<any>;
|
|
||||||
statistics$: Observable<any>;
|
statistics$: Observable<any>;
|
||||||
|
nodesRanking$: Observable<INodesRanking>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
|
@ -23,18 +23,7 @@ export class LightningDashboardComponent implements OnInit {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.seoService.setTitle($localize`Lightning Dashboard`);
|
this.seoService.setTitle($localize`Lightning Dashboard`);
|
||||||
|
|
||||||
const sharedObservable = this.lightningApiService.listTopNodes$().pipe(share());
|
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||||
|
|
||||||
this.nodesByCapacity$ = sharedObservable
|
|
||||||
.pipe(
|
|
||||||
map((object) => object.topByCapacity),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.nodesByChannels$ = sharedObservable
|
|
||||||
.pipe(
|
|
||||||
map((object) => object.topByChannels),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,9 @@ import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
|
||||||
import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component';
|
import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component';
|
||||||
import { NodesMap } from '../lightning/nodes-map/nodes-map.component';
|
import { NodesMap } from '../lightning/nodes-map/nodes-map.component';
|
||||||
import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels-map.component';
|
import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels-map.component';
|
||||||
|
import { NodesRanking } from '../lightning/nodes-ranking/nodes-ranking.component';
|
||||||
|
import { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component';
|
||||||
|
import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component';
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
LightningDashboardComponent,
|
LightningDashboardComponent,
|
||||||
|
@ -45,6 +48,9 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels
|
||||||
NodesPerCountryChartComponent,
|
NodesPerCountryChartComponent,
|
||||||
NodesMap,
|
NodesMap,
|
||||||
NodesChannelsMap,
|
NodesChannelsMap,
|
||||||
|
NodesRanking,
|
||||||
|
TopNodesPerChannels,
|
||||||
|
TopNodesPerCapacity,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -73,6 +79,9 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels
|
||||||
NodesPerCountryChartComponent,
|
NodesPerCountryChartComponent,
|
||||||
NodesMap,
|
NodesMap,
|
||||||
NodesChannelsMap,
|
NodesChannelsMap,
|
||||||
|
NodesRanking,
|
||||||
|
TopNodesPerChannels,
|
||||||
|
TopNodesPerCapacity,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LightningApiService,
|
LightningApiService,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { NodeComponent } from './node/node.component';
|
||||||
import { ChannelComponent } from './channel/channel.component';
|
import { ChannelComponent } from './channel/channel.component';
|
||||||
import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component';
|
import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component';
|
||||||
import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
|
import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component';
|
||||||
|
import { NodesRanking } from './nodes-ranking/nodes-ranking.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -32,6 +33,20 @@ const routes: Routes = [
|
||||||
path: 'nodes/isp/:isp',
|
path: 'nodes/isp/:isp',
|
||||||
component: NodesPerISP,
|
component: NodesPerISP,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'nodes/top-capacity',
|
||||||
|
component: NodesRanking,
|
||||||
|
data: {
|
||||||
|
type: 'capacity'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nodes/top-channels',
|
||||||
|
component: NodesRanking,
|
||||||
|
data: {
|
||||||
|
type: 'channels'
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
redirectTo: ''
|
redirectTo: ''
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<app-top-nodes-per-capacity [nodes$]="null" [widget]="false" *ngIf="type === 'capacity'">
|
||||||
|
</app-top-nodes-per-capacity>
|
||||||
|
|
||||||
|
<app-top-nodes-per-channels [nodes$]="null" [widget]="false" *ngIf="type === 'channels'">
|
||||||
|
</app-top-nodes-per-channels>
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-nodes-ranking',
|
||||||
|
templateUrl: './nodes-ranking.component.html',
|
||||||
|
styleUrls: ['./nodes-ranking.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class NodesRanking implements OnInit {
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.data.subscribe(data => {
|
||||||
|
this.type = data.type;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<div [class]="!widget ? 'container-xl full-height' : ''">
|
||||||
|
<h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-capacity">
|
||||||
|
<span>Top 100 nodes by capacity</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div [class]="widget ? 'widget' : 'full'">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<thead>
|
||||||
|
<th class="rank"></th>
|
||||||
|
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||||
|
<th class="capacity text-right" i18n="node.capacity">Capacity</th>
|
||||||
|
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||||
|
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
|
||||||
|
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||||
|
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
|
||||||
|
</thead>
|
||||||
|
<tbody *ngIf="topNodesPerCapacity$ | async as nodes; else skeleton">
|
||||||
|
<tr *ngFor="let node of nodes; let i = index;">
|
||||||
|
<td class="rank text-left">
|
||||||
|
{{ i + 1 }}
|
||||||
|
</td>
|
||||||
|
<td class="alias text-left">
|
||||||
|
<a [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">{{ node.alias }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="capacity text-right">
|
||||||
|
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="channels text-right">
|
||||||
|
{{ node.channels | number }}
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||||
|
{{ node?.city?.en ?? '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<ng-template #skeleton>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let item of skeletonRows">
|
||||||
|
<td class="rank text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="alias text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="capacity text-right">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="channels text-right">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</ng-template>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,84 @@
|
||||||
|
.container-xl {
|
||||||
|
max-width: 1400px;
|
||||||
|
padding-bottom: 100px;
|
||||||
|
@media (min-width: 767.98px) {
|
||||||
|
padding-left: 50px;
|
||||||
|
padding-right: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td, .table th {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .rank {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
.widget .rank {
|
||||||
|
@media (min-width: 767.98px) {
|
||||||
|
width: 13%;
|
||||||
|
}
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .alias {
|
||||||
|
width: 10%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 350px;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
max-width: 175px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widget .alias {
|
||||||
|
width: 55%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 350px;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
max-width: 175px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .capacity {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
.widget .capacity {
|
||||||
|
width: 32%;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .channels {
|
||||||
|
width: 15%;
|
||||||
|
padding-right: 50px;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .timestamp-first {
|
||||||
|
width: 15%;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .timestamp-update {
|
||||||
|
width: 15%;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .location {
|
||||||
|
width: 10%;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface';
|
||||||
|
import { LightningApiService } from '../../lightning-api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-top-nodes-per-capacity',
|
||||||
|
templateUrl: './top-nodes-per-capacity.component.html',
|
||||||
|
styleUrls: ['./top-nodes-per-capacity.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class TopNodesPerCapacity implements OnInit {
|
||||||
|
@Input() nodes$: Observable<INodesRanking>;
|
||||||
|
@Input() widget: boolean = false;
|
||||||
|
|
||||||
|
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
|
||||||
|
skeletonRows: number[] = [];
|
||||||
|
|
||||||
|
constructor(private apiService: LightningApiService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
|
||||||
|
this.skeletonRows.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.widget === false) {
|
||||||
|
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$();
|
||||||
|
} else {
|
||||||
|
this.topNodesPerCapacity$ = this.nodes$.pipe(
|
||||||
|
map((ranking) => {
|
||||||
|
return ranking.topByCapacity.slice(0, 10);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
<div [class]="!widget ? 'container-xl full-height' : ''">
|
||||||
|
<h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-channel">
|
||||||
|
<span>Top 100 nodes by channel count</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div [class]="widget ? 'widget' : 'full'">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<thead>
|
||||||
|
<th class="rank"></th>
|
||||||
|
<th class="alias text-left" i18n="nodes.alias">Alias</th>
|
||||||
|
<th class="channels text-right" i18n="node.channels">Channels</th>
|
||||||
|
<th *ngIf="!widget" class="capacity text-right" i18n="lightning.capacity">Capacity</th>
|
||||||
|
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
|
||||||
|
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
|
||||||
|
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
|
||||||
|
</thead>
|
||||||
|
<tbody *ngIf="topNodesPerChannels$ | async as nodes; else skeleton">
|
||||||
|
<tr *ngFor="let node of nodes; let i = index;">
|
||||||
|
<td class="rank text-left">
|
||||||
|
{{ i + 1 }}
|
||||||
|
</td>
|
||||||
|
<td class="alias text-left">
|
||||||
|
<a [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">{{ node.alias }}</a>
|
||||||
|
</td>
|
||||||
|
<td class="channels text-right">
|
||||||
|
{{ node.channels | number }}
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="capacity text-right">
|
||||||
|
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||||
|
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||||
|
{{ node?.city?.en ?? '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<ng-template #skeleton>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let item of skeletonRows">
|
||||||
|
<td class="rank text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="alias text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="channels text-right">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="capacity text-right">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="timestamp-first text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="timestamp-update text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td *ngIf="!widget" class="location text-right text-truncate">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</ng-template>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,84 @@
|
||||||
|
.container-xl {
|
||||||
|
max-width: 1400px;
|
||||||
|
padding-bottom: 100px;
|
||||||
|
@media (min-width: 767.98px) {
|
||||||
|
padding-left: 50px;
|
||||||
|
padding-right: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td, .table th {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .rank {
|
||||||
|
width: 5%;
|
||||||
|
}
|
||||||
|
.widget .rank {
|
||||||
|
@media (min-width: 767.98px) {
|
||||||
|
width: 13%;
|
||||||
|
}
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .alias {
|
||||||
|
width: 10%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 350px;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
max-width: 175px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.widget .alias {
|
||||||
|
width: 55%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 350px;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
max-width: 175px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .channels {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
.widget .channels {
|
||||||
|
width: 32%;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .capacity {
|
||||||
|
width: 15%;
|
||||||
|
padding-right: 50px;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .timestamp-first {
|
||||||
|
width: 15%;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .timestamp-update {
|
||||||
|
width: 15%;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full .location {
|
||||||
|
width: 10%;
|
||||||
|
@media (max-width: 767.98px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface';
|
||||||
|
import { LightningApiService } from '../../lightning-api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-top-nodes-per-channels',
|
||||||
|
templateUrl: './top-nodes-per-channels.component.html',
|
||||||
|
styleUrls: ['./top-nodes-per-channels.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class TopNodesPerChannels implements OnInit {
|
||||||
|
@Input() nodes$: Observable<INodesRanking>;
|
||||||
|
@Input() widget: boolean = false;
|
||||||
|
|
||||||
|
topNodesPerChannels$: Observable<ITopNodesPerChannels[]>;
|
||||||
|
skeletonRows: number[] = [];
|
||||||
|
|
||||||
|
constructor(private apiService: LightningApiService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
|
||||||
|
this.skeletonRows.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.widget === false) {
|
||||||
|
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$();
|
||||||
|
} else {
|
||||||
|
this.topNodesPerChannels$ = this.nodes$.pipe(
|
||||||
|
map((ranking) => {
|
||||||
|
return ranking.topByChannels.slice(0, 10);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue