diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index e59770d50..88b434711 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -2,6 +2,7 @@ import logger from '../../logger'; import DB from '../../database'; import { ResultSetHeader } from 'mysql2'; import { ILightningApi } from '../lightning/lightning-api.interface'; +import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces'; class NodesApi { public async $getNode(public_key: string): Promise { @@ -9,10 +10,10 @@ class NodesApi { // General info let query = ` SELECT public_key, alias, UNIX_TIMESTAMP(first_seen) AS first_seen, - UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets, - 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_country.names as country, geo_names_subdivision.names as subdivision + UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets, + 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_country.names as country, geo_names_subdivision.names as subdivision FROM nodes 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 @@ -112,20 +113,46 @@ class NodesApi { } } - public async $getTopCapacityNodes(): Promise { + public async $getTopCapacityNodes(full: boolean): Promise { try { let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats'); const latestDate = rows[0].maxAdded; - const query = ` - SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels - FROM node_stats - JOIN nodes ON nodes.public_key = node_stats.public_key - WHERE added = FROM_UNIXTIME(${latestDate}) - ORDER BY capacity DESC - LIMIT 10; - `; - [rows] = await DB.query(query); + let query: string; + 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 + JOIN nodes ON nodes.public_key = node_stats.public_key + WHERE added = FROM_UNIXTIME(${latestDate}) + ORDER BY capacity DESC + 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; } catch (e) { @@ -134,20 +161,94 @@ class NodesApi { } } - public async $getTopChannelsNodes(): Promise { + public async $getTopChannelsNodes(full: boolean): Promise { try { let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats'); const latestDate = rows[0].maxAdded; - const query = ` - SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels - FROM node_stats - JOIN nodes ON nodes.public_key = node_stats.public_key - WHERE added = FROM_UNIXTIME(${latestDate}) - ORDER BY channels DESC - LIMIT 10; - `; - [rows] = await DB.query(query); + let query: string; + 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 + JOIN nodes ON nodes.public_key = node_stats.public_key + WHERE added = FROM_UNIXTIME(${latestDate}) + ORDER BY channels DESC + 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.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; + } catch (e) { + logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + + public async $getOldestNodes(full: boolean): Promise { + try { + let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats'); + const latestDate = rows[0].maxAdded; + + let query: string; + 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 + JOIN nodes ON nodes.public_key = node_stats.public_key + WHERE added = FROM_UNIXTIME(${latestDate}) + ORDER BY first_seen + 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.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 first_seen + 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; } catch (e) { diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index a07001c8d..c5b442723 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -2,6 +2,7 @@ import config from '../../config'; import { Application, Request, Response } from 'express'; import nodesApi from './nodes.api'; import DB from '../../database'; +import { INodesRanking } from '../../mempool.interfaces'; class NodesRoutes { constructor() { } @@ -10,10 +11,13 @@ class NodesRoutes { app .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/top', this.$getTopNodes) .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/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/rankings/age', this.$getOldestNodes) .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) ; @@ -56,11 +60,14 @@ class NodesRoutes { } } - private async $getTopNodes(req: Request, res: Response) { + private async $getNodesRanking(req: Request, res: Response): Promise { try { - const topCapacityNodes = await nodesApi.$getTopCapacityNodes(); - const topChannelsNodes = await nodesApi.$getTopChannelsNodes(); - res.json({ + const topCapacityNodes = await nodesApi.$getTopCapacityNodes(false); + const topChannelsNodes = await nodesApi.$getTopChannelsNodes(false); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json({ topByCapacity: topCapacityNodes, topByChannels: topChannelsNodes, }); @@ -69,6 +76,42 @@ class NodesRoutes { } } + private async $getTopNodesByCapacity(req: Request, res: Response): Promise { + 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 { + 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 $getOldestNodes(req: Request, res: Response): Promise { + try { + const topCapacityNodes = await nodesApi.$getOldestNodes(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 { try { const nodesPerAs = await nodesApi.$getNodesISPRanking(); diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index c2d2ee747..d72b13576 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -251,3 +251,41 @@ export interface RewardStats { totalFee: 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[]; +} + +export interface IOldestNodes { + publicKey: string, + alias: string, + firstSeen: number, + channels?: number, + capacity: number, + updatedAt?: number, + city?: any, + country?: any, +} \ No newline at end of file diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 5f3506db5..838208cc3 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -151,3 +151,41 @@ export interface RewardStats { totalFee: 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[]; +} + +export interface IOldestNodes { + publicKey: string, + alias: string, + firstSeen: number, + channels?: number, + capacity: number, + updatedAt?: number, + city?: any, + country?: any, +} \ No newline at end of file diff --git a/frontend/src/app/lightning/lightning-api.service.ts b/frontend/src/app/lightning/lightning-api.service.ts index 2a6634558..f14b0b382 100644 --- a/frontend/src/app/lightning/lightning-api.service.ts +++ b/frontend/src/app/lightning/lightning-api.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import { StateService } from '../services/state.service'; +import { INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesPerChannels } from '../interfaces/node-api.interface'; @Injectable({ providedIn: 'root' @@ -48,8 +49,8 @@ export class LightningApiService { return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics'); } - listTopNodes$(): Observable { - return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/top'); + getNodesRanking$(): Observable { + return this.httpClient.get(this.apiBasePath + '/api/v1/lightning/nodes/rankings'); } listChannelStats$(publicKey: string): Observable { @@ -62,4 +63,22 @@ export class LightningApiService { (interval !== undefined ? `/${interval}` : ''), { observe: 'response' } ); } + + getTopNodesByCapacity$(): Observable { + return this.httpClient.get( + this.apiBasePath + '/api/v1/lightning/nodes/rankings/capacity' + ); + } + + getTopNodesByChannels$(): Observable { + return this.httpClient.get( + this.apiBasePath + '/api/v1/lightning/nodes/rankings/channels' + ); + } + + getOldestNodes$(): Observable { + return this.httpClient.get( + this.apiBasePath + '/api/v1/lightning/nodes/rankings/age' + ); + } } diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index ff00f5b15..fe678a2fc 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -42,6 +42,7 @@ +
@@ -53,22 +54,30 @@
+
-
Top Capacity Nodes
- - + +
Top capacity nodes
+   + +
+
+
-
Most Connected Nodes
- - + +
Most connected nodes
+   + +
+
diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index 5d4685fb8..d601606fd 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 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 { LightningApiService } from '../lightning-api.service'; @@ -11,9 +12,8 @@ import { LightningApiService } from '../lightning-api.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class LightningDashboardComponent implements OnInit { - nodesByCapacity$: Observable; - nodesByChannels$: Observable; statistics$: Observable; + nodesRanking$: Observable; constructor( private lightningApiService: LightningApiService, @@ -23,18 +23,7 @@ export class LightningDashboardComponent implements OnInit { ngOnInit(): void { this.seoService.setTitle($localize`Lightning Dashboard`); - const sharedObservable = this.lightningApiService.listTopNodes$().pipe(share()); - - this.nodesByCapacity$ = sharedObservable - .pipe( - map((object) => object.topByCapacity), - ); - - this.nodesByChannels$ = sharedObservable - .pipe( - map((object) => object.topByChannels), - ); - + this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share()); this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); } diff --git a/frontend/src/app/lightning/lightning.module.ts b/frontend/src/app/lightning/lightning.module.ts index c01792815..7ca02b2ba 100644 --- a/frontend/src/app/lightning/lightning.module.ts +++ b/frontend/src/app/lightning/lightning.module.ts @@ -24,6 +24,12 @@ import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component'; import { NodesPerCountryChartComponent } from '../lightning/nodes-per-country-chart/nodes-per-country-chart.component'; import { NodesMap } from '../lightning/nodes-map/nodes-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'; +import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component'; +import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component'; + @NgModule({ declarations: [ LightningDashboardComponent, @@ -45,6 +51,11 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels NodesPerCountryChartComponent, NodesMap, NodesChannelsMap, + NodesRanking, + TopNodesPerChannels, + TopNodesPerCapacity, + OldestNodes, + NodesRankingsDashboard, ], imports: [ CommonModule, @@ -73,6 +84,11 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels NodesPerCountryChartComponent, NodesMap, NodesChannelsMap, + NodesRanking, + TopNodesPerChannels, + TopNodesPerCapacity, + OldestNodes, + NodesRankingsDashboard, ], providers: [ LightningApiService, diff --git a/frontend/src/app/lightning/lightning.routing.module.ts b/frontend/src/app/lightning/lightning.routing.module.ts index 8bfb467af..13fd20b6a 100644 --- a/frontend/src/app/lightning/lightning.routing.module.ts +++ b/frontend/src/app/lightning/lightning.routing.module.ts @@ -6,6 +6,8 @@ import { NodeComponent } from './node/node.component'; import { ChannelComponent } from './channel/channel.component'; import { NodesPerCountry } from './nodes-per-country/nodes-per-country.component'; import { NodesPerISP } from './nodes-per-isp/nodes-per-isp.component'; +import { NodesRanking } from './nodes-ranking/nodes-ranking.component'; +import { NodesRankingsDashboard } from './nodes-rankings-dashboard/nodes-rankings-dashboard.component'; const routes: Routes = [ { @@ -32,6 +34,31 @@ const routes: Routes = [ path: 'nodes/isp/:isp', component: NodesPerISP, }, + { + path: 'nodes/rankings', + component: NodesRankingsDashboard, + }, + { + path: 'nodes/top-capacity', + component: NodesRanking, + data: { + type: 'capacity' + }, + }, + { + path: 'nodes/top-channels', + component: NodesRanking, + data: { + type: 'channels' + }, + }, + { + path: 'nodes/oldest', + component: NodesRanking, + data: { + type: 'oldest' + }, + }, { path: '**', redirectTo: '' diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html new file mode 100644 index 000000000..5bd03941e --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.scss b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts new file mode 100644 index 000000000..373751be9 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts @@ -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; + }); + } +} diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html new file mode 100644 index 000000000..5b96400c2 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.html @@ -0,0 +1,71 @@ +
+

+ Top 100 oldest lightning nodes +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AliasFirst seenCapacityChannelsLast updateLocation
+ {{ i + 1 }} + + {{ node.alias }} + + ‎{{ node.firstSeen * 1000 | date: 'yyyy-MM-dd' }} + + + + {{ node.channels | number }} + + + + {{ node?.city?.en ?? '-' }} +
+ + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.scss b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.scss new file mode 100644 index 000000000..5f77ab41b --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.scss @@ -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: 50%; + overflow: hidden; + text-overflow: ellipsis; + max-width: 300px; + @media (max-width: 767.98px) { + max-width: 170px; + } +} + +.full .capacity { + width: 10%; + @media (max-width: 767.98px) { + display: none; + } +} +.widget .capacity { + width: 10%; + @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: 10%; +} + +.full .timestamp-update { + width: 20%; + @media (max-width: 767.98px) { + display: none; + } +} + +.full .location { + width: 10%; + @media (max-width: 767.98px) { + display: none; + } +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts new file mode 100644 index 000000000..23f248b0e --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/oldest-nodes/oldest-nodes.component.ts @@ -0,0 +1,36 @@ +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { map, Observable } from 'rxjs'; +import { IOldestNodes } from '../../../interfaces/node-api.interface'; +import { LightningApiService } from '../../lightning-api.service'; + +@Component({ + selector: 'app-oldest-nodes', + templateUrl: './oldest-nodes.component.html', + styleUrls: ['./oldest-nodes.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OldestNodes implements OnInit { + @Input() widget: boolean = false; + + oldestNodes$: Observable; + 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.oldestNodes$ = this.apiService.getOldestNodes$(); + } else { + this.oldestNodes$ = this.apiService.getOldestNodes$().pipe( + map((nodes: IOldestNodes[]) => { + return nodes.slice(0, 10); + }) + ); + } + } + +} diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html new file mode 100644 index 000000000..a9043f9d1 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html @@ -0,0 +1,71 @@ +
+

+ Top 100 nodes by capacity +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AliasCapacityChannelsFirst seenLast updateLocation
+ {{ i + 1 }} + + {{ node.alias }} + + + + {{ node.channels | number }} + + + + + + {{ node?.city?.en ?? '-' }} +
+ + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss new file mode 100644 index 000000000..2b927db7f --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts new file mode 100644 index 000000000..2d90817f9 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -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; + @Input() widget: boolean = false; + + topNodesPerCapacity$: Observable; + 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); + }) + ); + } + } + +} diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html new file mode 100644 index 000000000..72c2aa5b1 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html @@ -0,0 +1,71 @@ +
+

+ Top 100 nodes by channel count +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AliasChannelsCapacityFirst seenLast updateLocation
+ {{ i + 1 }} + + {{ node.alias }} + + {{ node.channels | number }} + + + + + + + + {{ node?.city?.en ?? '-' }} +
+ + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss new file mode 100644 index 000000000..5af0e8339 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts new file mode 100644 index 000000000..c2821c596 --- /dev/null +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -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; + @Input() widget: boolean = false; + + topNodesPerChannels$: Observable; + 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); + }) + ); + } + } + +} diff --git a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html new file mode 100644 index 000000000..93f7d1fc3 --- /dev/null +++ b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.html @@ -0,0 +1,47 @@ + \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.scss b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.scss new file mode 100644 index 000000000..28e80d451 --- /dev/null +++ b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.scss @@ -0,0 +1,33 @@ +.main { + max-width: 90%; +} + +.col { + padding-bottom: 20px; + padding-left: 10px; + padding-right: 10px; +} + +.card { + background-color: #1d1f31; +} + +.card-title { + font-size: 1rem; + color: #4a68b9; +} +.card-title > a { + color: #4a68b9; +} + +.card-text { + font-size: 22px; +} + +.title-link, .title-link:hover, .title-link:focus, .title-link:active { + text-align: center; + display: block; + margin-bottom: 10px; + text-decoration: none; + color: inherit; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts new file mode 100644 index 000000000..4b39d8467 --- /dev/null +++ b/frontend/src/app/lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { Observable, share } from 'rxjs'; +import { INodesRanking } from 'src/app/interfaces/node-api.interface'; +import { SeoService } from 'src/app/services/seo.service'; +import { LightningApiService } from '../lightning-api.service'; + +@Component({ + selector: 'app-nodes-rankings-dashboard', + templateUrl: './nodes-rankings-dashboard.component.html', + styleUrls: ['./nodes-rankings-dashboard.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NodesRankingsDashboard implements OnInit { + nodesRanking$: Observable; + + constructor( + private lightningApiService: LightningApiService, + private seoService: SeoService, + ) {} + + ngOnInit(): void { + this.seoService.setTitle($localize`Top lightning nodes`); + this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share()); + } +}