Create top 100 node per capacity component

This commit is contained in:
nymkappa 2022-08-17 16:00:30 +02:00
parent 7520e3beba
commit 2359e44b16
No known key found for this signature in database
GPG key ID: E155910B16E8BD04
13 changed files with 216 additions and 94 deletions

View file

@ -10,10 +10,10 @@ class NodesApi {
// General info // General info
let query = ` let query = `
SELECT public_key, alias, UNIX_TIMESTAMP(first_seen) AS first_seen, SELECT public_key, alias, UNIX_TIMESTAMP(first_seen) AS first_seen,
UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets, UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets,
as_number, city_id, country_id, subdivision_id, longitude, latitude, as_number, city_id, country_id, subdivision_id, longitude, latitude,
geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city, geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
geo_names_country.names as country, geo_names_subdivision.names as subdivision geo_names_country.names as country, geo_names_subdivision.names as subdivision
FROM nodes FROM nodes
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
@ -113,21 +113,46 @@ class NodesApi {
} }
} }
public async $getTopCapacityNodes(): Promise<TopNodesPerCapacity[]> { public async $getTopCapacityNodes(full: boolean): Promise<TopNodesPerCapacity[]> {
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, if (full === false) {
node_stats.capacity query = `
FROM node_stats SELECT nodes.public_key AS publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
JOIN nodes ON nodes.public_key = node_stats.public_key node_stats.capacity
WHERE added = FROM_UNIXTIME(${latestDate}) FROM node_stats
ORDER BY capacity DESC JOIN nodes ON nodes.public_key = node_stats.public_key
LIMIT 100; WHERE added = FROM_UNIXTIME(${latestDate})
`; ORDER BY capacity DESC
[rows] = await DB.query(query); LIMIT 100
`;
[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) {

View file

@ -11,10 +11,11 @@ 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/rankings', this.$getNodesRanking)
.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/: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)
; ;
@ -59,8 +60,11 @@ class NodesRoutes {
private async $getNodesRanking(req: Request, res: Response): Promise<void> { 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();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString());
res.json(<INodesRanking>{ res.json(<INodesRanking>{
topByCapacity: topCapacityNodes, topByCapacity: topCapacityNodes,
topByChannels: topChannelsNodes, topByChannels: topChannelsNodes,
@ -70,6 +74,18 @@ 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 $getISPRanking(req: Request, res: Response): Promise<void> { private async $getISPRanking(req: Request, res: Response): Promise<void> {
try { try {
const groupBy = req.query.groupBy as string; const groupBy = req.query.groupBy as string;

View file

@ -259,9 +259,14 @@ export interface TopNodesPerChannels {
} }
export interface TopNodesPerCapacity { export interface TopNodesPerCapacity {
public_key: string, publicKey: string,
alias: string, alias: string,
capacity: number, capacity: number,
channels?: number,
firstSeen?: number,
updatedAt?: number,
city?: any,
country?: any,
} }
export interface INodesRanking { export interface INodesRanking {

View file

@ -159,9 +159,14 @@ export interface ITopNodesPerChannels {
} }
export interface ITopNodesPerCapacity { export interface ITopNodesPerCapacity {
public_key: string, publicKey: string,
alias: string, alias: string,
capacity: number, capacity: number,
channels?: number,
firstSeen?: number,
updatedAt?: number,
city?: any,
country?: any,
} }
export interface INodesRanking { export interface INodesRanking {

View file

@ -2,7 +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 } from '../interfaces/node-api.interface'; import { INodesRanking, ITopNodesPerCapacity } from '../interfaces/node-api.interface';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -63,4 +63,10 @@ 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'
);
}
} }

View file

@ -56,8 +56,8 @@
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/lightning/nodes/ranking' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/lightning/nodes/top-capacity' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.top-capacity-nodes">Top Capacity Nodes</h5> <h5 class="card-title d-inline" i18n="lightning.top-capacity-nodes">Top capacity nodes</h5>
<span>&nbsp;</span> <span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>
</a> </a>
@ -70,7 +70,7 @@
<div class="col"> <div class="col">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<a class="title-link" href="" [routerLink]="['/lightning/nodes/ranking' | relativeUrl]"> <a class="title-link" href="" [routerLink]="['/lightning/nodes/top-channels' | relativeUrl]">
<h5 class="card-title d-inline" i18n="lightning.top-channels-nodes">Most connected nodes</h5> <h5 class="card-title d-inline" i18n="lightning.top-channels-nodes">Most connected nodes</h5>
<span>&nbsp;</span> <span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: '#4a68b9'"></fa-icon>

View file

@ -24,6 +24,7 @@ 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 { TopNodesPerChannels } from '../lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component';
import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component'; import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component';
@NgModule({ @NgModule({
@ -47,6 +48,7 @@ import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-ca
NodesPerCountryChartComponent, NodesPerCountryChartComponent,
NodesMap, NodesMap,
NodesChannelsMap, NodesChannelsMap,
NodesRanking,
TopNodesPerChannels, TopNodesPerChannels,
TopNodesPerCapacity, TopNodesPerCapacity,
], ],
@ -77,6 +79,7 @@ import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-ca
NodesPerCountryChartComponent, NodesPerCountryChartComponent,
NodesMap, NodesMap,
NodesChannelsMap, NodesChannelsMap,
NodesRanking,
TopNodesPerChannels, TopNodesPerChannels,
TopNodesPerCapacity, TopNodesPerCapacity,
], ],

View file

@ -34,8 +34,11 @@ const routes: Routes = [
component: NodesPerISP, component: NodesPerISP,
}, },
{ {
path: 'nodes/ranking', path: 'nodes/top-capacity',
component: NodesRanking, component: NodesRanking,
data: {
type: 'capacity'
},
}, },
{ {
path: '**', path: '**',

View file

@ -1,29 +1,2 @@
<div style="width: 100%"> <app-top-nodes-per-capacity [nodes$]="null" [widget]="false" *ngIf="type === 'capacity'">
</app-top-nodes-per-capacity>
<table class="table table-borderless" style="width: 100%">
<thead>
<th class="" i18n="" style="width: 33%">Rank</th>
<th class="" i18n="" style="width: 33%">Alias</th>
<th class="" i18n="" style="width: 33%">Placeholder</th>
</thead>
<tbody>
</tbody>
</table>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
<td class="">
<span class="skeleton-loader"></span>
</td>
<td class="">
<span class="skeleton-loader"></span>
</td>
<td class="">
<span class="skeleton-loader"></span>
</td>
</tr>
</tbody>
</ng-template>
</div>

View file

@ -1,4 +1,5 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'app-nodes-ranking', selector: 'app-nodes-ranking',
@ -7,9 +8,13 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NodesRanking implements OnInit { export class NodesRanking implements OnInit {
type: string;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void { ngOnInit(): void {
console.log('hi'); this.route.data.subscribe(data => {
this.type = data.type;
});
} }
} }

View file

@ -1,37 +1,59 @@
<div> <div [class]="!widget ? 'container-xl full-height' : ''">
<table class="table table-borderless"> <h1 *ngIf="!widget" class="float-left" i18n="lightning.top-100-capacity">
<thead> <span>Top 100 nodes by capacity</span>
<th class="rank"></th> </h1>
<th class="alias text-left" i18n="nodes.alias">Alias</th>
<th class="capacity text-right" i18n="node.capacity">Capacity</th> <div [class]="widget ? 'widget' : 'full'">
</thead> <table class="table table-borderless">
<tbody *ngIf="topNodesPerCapacity$ | async as nodes; else skeleton"> <thead>
<tr *ngFor="let node of nodes; let i = index;"> <th class="rank"></th>
<td class="rank text-left"> <th class="alias text-left" i18n="nodes.alias">Alias</th>
{{ i + 1 }} <th class="capacity text-right" i18n="node.capacity">Capacity</th>
</td> <th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
<td class="alias text-left"> <th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<a [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">{{ node.alias }}</a> <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
</td> <th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
<td class="capacity text-right"> </thead>
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <tbody *ngIf="topNodesPerCapacity$ | async as nodes; else skeleton">
</td> <tr *ngFor="let node of nodes; let i = index;">
</tr>
</tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonRows">
<td class="rank text-left"> <td class="rank text-left">
<span class="skeleton-loader"></span> {{ i + 1 }}
</td> </td>
<td class="alias text-left"> <td class="alias text-left">
<span class="skeleton-loader"></span> <a [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">{{ node.alias }}</a>
</td> </td>
<td class="capacity text-right"> <td class="capacity text-right">
<span class="skeleton-loader"></span> <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> </td>
</tr> </tr>
</tbody> </tbody>
</ng-template> <ng-template #skeleton>
</table> <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>
</tr>
</tbody>
</ng-template>
</table>
</div>
</div> </div>

View file

@ -1,8 +1,20 @@
.container-xl {
max-width: 1400px;
padding-bottom: 100px;
@media (min-width: 767.98px) {
padding-left: 50px;
padding-right: 50px;
}
}
.table td, .table th { .table td, .table th {
padding: 0.5rem; padding: 0.5rem;
} }
.rank { .full .rank {
width: 5%;
}
.widget .rank {
@media (min-width: 767.98px) { @media (min-width: 767.98px) {
width: 13%; width: 13%;
} }
@ -12,7 +24,16 @@
} }
} }
.alias { .full .alias {
width: 10%;
overflow: hidden;
text-overflow: ellipsis;
max-width: 350px;
@media (max-width: 767.98px) {
max-width: 175px;
}
}
.widget .alias {
width: 55%; width: 55%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -21,10 +42,43 @@
max-width: 175px; max-width: 175px;
} }
} }
.capacity {
.full .capacity {
width: 10%;
}
.widget .capacity {
width: 32%; width: 32%;
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
padding-left: 0px; padding-left: 0px;
padding-right: 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;
}
}

View file

@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface'; import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface';
import { LightningApiService } from '../../lightning-api.service';
@Component({ @Component({
selector: 'app-top-nodes-per-capacity', selector: 'app-top-nodes-per-capacity',
@ -15,20 +16,24 @@ export class TopNodesPerCapacity implements OnInit {
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>; topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
skeletonRows: number[] = []; skeletonRows: number[] = [];
constructor(private apiService: LightningApiService) {}
ngOnInit(): void { ngOnInit(): void {
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) { for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
this.skeletonRows.push(i); this.skeletonRows.push(i);
} }
this.topNodesPerCapacity$ = this.nodes$.pipe( console.log(this.widget);
map((ranking) => {
if (this.widget === true) { if (this.widget === false) {
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$();
} else {
this.topNodesPerCapacity$ = this.nodes$.pipe(
map((ranking) => {
return ranking.topByCapacity.slice(0, 10); return ranking.topByCapacity.slice(0, 10);
} else { })
return ranking.topByCapacity; );
} }
})
)
} }
} }