Merge pull request #4460 from natsee/lightning-dashboard-ownership

Add % ownership in capacity table (LN dashboard)
This commit is contained in:
softsimon 2024-01-18 16:41:21 +07:00 committed by GitHub
commit e373f36765
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 121 additions and 33 deletions

View File

@ -1,3 +1,3 @@
I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023. I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of November 16, 2023.
Signed: ncois Signed: natsee

View File

@ -255,6 +255,29 @@ export interface INodesRanking {
topByChannels: ITopNodesPerChannels[]; topByChannels: ITopNodesPerChannels[];
} }
export interface INodesStatisticsEntry {
added: string;
avg_base_fee_mtokens: number;
avg_capacity: number;
avg_fee_rate: number;
channel_count: number;
clearnet_nodes: number;
clearnet_tor_nodes: number;
id: number;
med_base_fee_mtokens: number;
med_capacity: number;
med_fee_rate: number;
node_count: number;
tor_nodes: number;
total_capacity: number;
unannounced_nodes: number;
}
export interface INodesStatistics {
latest: INodesStatisticsEntry;
previous: INodesStatisticsEntry;
}
export interface IOldestNodes { export interface IOldestNodes {
publicKey: string, publicKey: string,
alias: string, alias: string,

View File

@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { INodesStatistics } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-channels-statistics', selector: 'app-channels-statistics',
@ -8,7 +9,7 @@ import { Observable } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ChannelsStatisticsComponent implements OnInit { export class ChannelsStatisticsComponent implements OnInit {
@Input() statistics$: Observable<any>; @Input() statistics$: Observable<INodesStatistics>;
mode: string = 'avg'; mode: string = 'avg';
constructor() { } constructor() { }

View File

@ -63,7 +63,7 @@
<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>
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity> <app-top-nodes-per-capacity [nodes$]="nodesRanking$" [statistics$]="statistics$" [widget]="true"></app-top-nodes-per-capacity>
</div> </div>
</div> </div>
</div> </div>
@ -77,7 +77,7 @@
<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>
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels> <app-top-nodes-per-channels [nodes$]="nodesRanking$" [statistics$]="statistics$" [widget]="true"></app-top-nodes-per-channels>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { share } from 'rxjs/operators'; import { share } from 'rxjs/operators';
import { INodesRanking } from '../../interfaces/node-api.interface'; import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface';
import { SeoService } from '../../services/seo.service'; import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { LightningApiService } from '../lightning-api.service'; import { LightningApiService } from '../lightning-api.service';
@ -13,7 +13,7 @@ import { LightningApiService } from '../lightning-api.service';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class LightningDashboardComponent implements OnInit, AfterViewInit { export class LightningDashboardComponent implements OnInit, AfterViewInit {
statistics$: Observable<any>; statistics$: Observable<INodesStatistics>;
nodesRanking$: Observable<INodesRanking>; nodesRanking$: Observable<INodesRanking>;
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;

View File

@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { INodesStatistics } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-node-statistics', selector: 'app-node-statistics',
@ -8,7 +9,7 @@ import { Observable } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NodeStatisticsComponent implements OnInit { export class NodeStatisticsComponent implements OnInit {
@Input() statistics$: Observable<any>; @Input() statistics$: Observable<INodesStatistics>;
constructor() { } constructor() { }

View File

@ -1,7 +1,7 @@
<app-top-nodes-per-capacity [nodes$]="null" [widget]="false" *ngIf="type === 'capacity'"> <app-top-nodes-per-capacity [nodes$]="null" [statistics$]="statistics$" [widget]="false" *ngIf="type === 'capacity'">
</app-top-nodes-per-capacity> </app-top-nodes-per-capacity>
<app-top-nodes-per-channels [nodes$]="null" [widget]="false" *ngIf="type === 'channels'"> <app-top-nodes-per-channels [nodes$]="null" [statistics$]="statistics$" [widget]="false" *ngIf="type === 'channels'">
</app-top-nodes-per-channels> </app-top-nodes-per-channels>
<app-oldest-nodes [widget]="false" *ngIf="type === 'oldest'"></app-oldest-nodes> <app-oldest-nodes [widget]="false" *ngIf="type === 'oldest'"></app-oldest-nodes>

View File

@ -1,5 +1,9 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { LightningApiService } from '../lightning-api.service';
import { share } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { INodesStatistics } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-nodes-ranking', selector: 'app-nodes-ranking',
@ -9,10 +13,15 @@ import { ActivatedRoute } from '@angular/router';
}) })
export class NodesRanking implements OnInit { export class NodesRanking implements OnInit {
type: string; type: string;
statistics$: Observable<INodesStatistics>;
constructor(private route: ActivatedRoute) {} constructor(
private route: ActivatedRoute,
private lightningApiService: LightningApiService,
) {}
ngOnInit(): void { ngOnInit(): void {
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
this.route.data.subscribe(data => { this.route.data.subscribe(data => {
this.type = data.type; this.type = data.type;
}); });

View File

@ -16,8 +16,8 @@
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th> <th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
<th *ngIf="!widget" class="d-none d-md-table-cell text-right" i18n="lightning.location">Location</th> <th *ngIf="!widget" class="d-none d-md-table-cell text-right" i18n="lightning.location">Location</th>
</thead> </thead>
<tbody *ngIf="topNodesPerCapacity$ | async as nodes"> <tbody *ngIf="topNodesPerCapacity$ | async as data">
<tr *ngFor="let node of nodes;"> <tr *ngFor="let node of data.nodes;">
<td class="pool text-left"> <td class="pool text-left">
<div class="tooltip-custom d-block w-100"> <div class="tooltip-custom d-block w-100">
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]"> <a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
@ -27,12 +27,14 @@
</td> </td>
<td class="text-right"> <td class="text-right">
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<span class="capacity-ratio">&nbsp;({{ (node?.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%)</span>
</td> </td>
<td class="d-table-cell fiat text-right" [ngClass]="{'widget': widget}"> <td class="d-table-cell fiat text-right" [ngClass]="{'widget': widget}">
<app-fiat [value]="node.capacity"></app-fiat> <app-fiat [value]="node.capacity"></app-fiat>
</td> </td>
<td *ngIf="!widget" class="d-none d-md-table-cell text-right"> <td *ngIf="!widget" class="d-none d-md-table-cell text-right">
{{ node.channels | number }} {{ node.channels | number }}
<span class="capacity-ratio">&nbsp;({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%)</span>
</td> </td>
<td *ngIf="!widget" class="d-none d-md-table-cell text-right"> <td *ngIf="!widget" class="d-none d-md-table-cell text-right">
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp> <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>

View File

@ -41,6 +41,11 @@ tr, td, th {
} }
} }
.capacity-ratio {
font-size: 12px;
color: darkgrey;
}
.fiat { .fiat {
width: 15%; width: 15%;
@media (min-width: 768px) and (max-width: 991px) { @media (min-width: 768px) and (max-width: 991px) {

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs'; import { combineLatest, map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface'; import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '../../../services/seo.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
@ -14,9 +14,10 @@ import { LightningApiService } from '../../lightning-api.service';
}) })
export class TopNodesPerCapacity implements OnInit { export class TopNodesPerCapacity implements OnInit {
@Input() nodes$: Observable<INodesRanking>; @Input() nodes$: Observable<INodesRanking>;
@Input() statistics$: Observable<INodesStatistics>;
@Input() widget: boolean = false; @Input() widget: boolean = false;
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>; topNodesPerCapacity$: Observable<{ nodes: ITopNodesPerCapacity[]; statistics: { totalCapacity: number; totalChannels?: number; } }>;
skeletonRows: number[] = []; skeletonRows: number[] = [];
currency$: Observable<string>; currency$: Observable<string>;
@ -39,8 +40,12 @@ export class TopNodesPerCapacity implements OnInit {
} }
if (this.widget === false) { if (this.widget === false) {
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe( this.topNodesPerCapacity$ = combineLatest([
map((ranking) => { this.apiService.getTopNodesByCapacity$(),
this.statistics$
])
.pipe(
map(([ranking, statistics]) => {
for (const i in ranking) { for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{ ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en, country: ranking[i].country?.en,
@ -49,13 +54,28 @@ export class TopNodesPerCapacity implements OnInit {
iso: ranking[i].iso_code, iso: ranking[i].iso_code,
}; };
} }
return ranking; return {
nodes: ranking,
statistics: {
totalCapacity: statistics.latest.total_capacity,
totalChannels: statistics.latest.channel_count,
}
}
}) })
); );
} else { } else {
this.topNodesPerCapacity$ = this.nodes$.pipe( this.topNodesPerCapacity$ = combineLatest([
map((ranking) => { this.nodes$,
return ranking.topByCapacity.slice(0, 6); this.statistics$
])
.pipe(
map(([ranking, statistics]) => {
return {
nodes: ranking.topByCapacity.slice(0, 6),
statistics: {
totalCapacity: statistics.latest.total_capacity,
}
}
}) })
); );
} }

View File

@ -16,8 +16,8 @@
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th> <th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
<th class="geolocation d-table-cell text-right" i18n="lightning.location">Location</th> <th class="geolocation d-table-cell text-right" i18n="lightning.location">Location</th>
</thead> </thead>
<tbody *ngIf="topNodesPerChannels$ | async as nodes"> <tbody *ngIf="topNodesPerChannels$ | async as data">
<tr *ngFor="let node of nodes;"> <tr *ngFor="let node of data.nodes;">
<td class="pool text-left"> <td class="pool text-left">
<div class="tooltip-custom d-block w-100"> <div class="tooltip-custom d-block w-100">
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]"> <a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
@ -27,9 +27,11 @@
</td> </td>
<td class="text-right"> <td class="text-right">
{{ node.channels ? (node.channels | number) : '~' }} {{ node.channels ? (node.channels | number) : '~' }}
<span class="capacity-ratio">&nbsp;({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%)</span>
</td> </td>
<td *ngIf="!widget" class="d-none d-md-table-cell capacity text-right"> <td *ngIf="!widget" class="d-none d-md-table-cell capacity text-right">
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<span class="capacity-ratio">&nbsp;({{ (node.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%)</span>
</td> </td>
<td *ngIf="!widget" class="fiat d-none d-md-table-cell text-right"> <td *ngIf="!widget" class="fiat d-none d-md-table-cell text-right">
<app-fiat [value]="node.capacity"></app-fiat> <app-fiat [value]="node.capacity"></app-fiat>

View File

@ -44,6 +44,11 @@ tr, td, th {
} }
} }
.capacity-ratio {
font-size: 12px;
color: darkgrey;
}
.geolocation { .geolocation {
@media (min-width: 768px) and (max-width: 991px) { @media (min-width: 768px) and (max-width: 991px) {
display: none !important; display: none !important;

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs'; import { combineLatest, map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface'; import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
import { SeoService } from '../../../services/seo.service'; import { SeoService } from '../../../services/seo.service';
import { StateService } from '../../../services/state.service'; import { StateService } from '../../../services/state.service';
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
@ -14,9 +14,10 @@ import { LightningApiService } from '../../lightning-api.service';
}) })
export class TopNodesPerChannels implements OnInit { export class TopNodesPerChannels implements OnInit {
@Input() nodes$: Observable<INodesRanking>; @Input() nodes$: Observable<INodesRanking>;
@Input() statistics$: Observable<INodesStatistics>;
@Input() widget: boolean = false; @Input() widget: boolean = false;
topNodesPerChannels$: Observable<ITopNodesPerChannels[]>; topNodesPerChannels$: Observable<{ nodes: ITopNodesPerChannels[]; statistics: { totalChannels: number; totalCapacity?: number; } }>;
skeletonRows: number[] = []; skeletonRows: number[] = [];
currency$: Observable<string>; currency$: Observable<string>;
@ -37,8 +38,12 @@ export class TopNodesPerChannels implements OnInit {
this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`); this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`);
this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.channels:See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.`); this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.channels:See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.`);
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe( this.topNodesPerChannels$ = combineLatest([
map((ranking) => { this.apiService.getTopNodesByChannels$(),
this.statistics$
])
.pipe(
map(([ranking, statistics]) => {
for (const i in ranking) { for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{ ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en, country: ranking[i].country?.en,
@ -47,12 +52,22 @@ export class TopNodesPerChannels implements OnInit {
iso: ranking[i].iso_code, iso: ranking[i].iso_code,
}; };
} }
return ranking; return {
nodes: ranking,
statistics: {
totalChannels: statistics.latest.channel_count,
totalCapacity: statistics.latest.total_capacity,
}
}
}) })
); );
} else { } else {
this.topNodesPerChannels$ = this.nodes$.pipe( this.topNodesPerChannels$ = combineLatest([
map((ranking) => { this.nodes$,
this.statistics$
])
.pipe(
map(([ranking, statistics]) => {
for (const i in ranking.topByChannels) { for (const i in ranking.topByChannels) {
ranking.topByChannels[i].geolocation = <GeolocationData>{ ranking.topByChannels[i].geolocation = <GeolocationData>{
country: ranking.topByChannels[i].country?.en, country: ranking.topByChannels[i].country?.en,
@ -61,7 +76,12 @@ export class TopNodesPerChannels implements OnInit {
iso: ranking.topByChannels[i].iso_code, iso: ranking.topByChannels[i].iso_code,
}; };
} }
return ranking.topByChannels.slice(0, 6); return {
nodes: ranking.topByChannels.slice(0, 6),
statistics: {
totalChannels: statistics.latest.channel_count,
}
}
}) })
); );
} }