Mempool node group page
This commit is contained in:
123
frontend/src/app/lightning/group/group.component.html
Normal file
123
frontend/src/app/lightning/group/group.component.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<div class="container-xl full-height" style="min-height: 335px">
|
||||
<div class="header">
|
||||
<div class="logo-container">
|
||||
<app-svg-images name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="box">
|
||||
<div class="row" *ngIf="nodes$ | async as nodes">
|
||||
<div class="col-12 col-md-6">
|
||||
<table class="table table-borderless table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n="lightning.node-count">Nodes</td>
|
||||
<td>{{ nodes.nodes.length }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.liquidity">Liquidity</td>
|
||||
<td>
|
||||
<app-amount *ngIf="nodes.sumLiquidity > 100000000; else smallnode" [satoshis]="nodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
{{ nodes.sumLiquidity | amountShortener: 1 }}
|
||||
<span class="sats" i18n="shared.sats">sats</span>
|
||||
</ng-template>
|
||||
<span class="d-none d-md-inline-block"> </span>
|
||||
<span class="d-block d-md-none"></span>
|
||||
<app-fiat [value]="nodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="lightning.channels">Channels</td>
|
||||
<td>{{ nodes.sumChannels }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 p-3 p-md-0 pr-md-3">
|
||||
<div style="background-color: #181b2d">
|
||||
<app-nodes-map [widget]="true" [nodes]="nodes.nodes" type="isp"></app-nodes-map>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="toggle-holder">
|
||||
<form [formGroup]="socketToggleForm" class="formRadioGroup">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="socket">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="0">IPv4
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
<input ngbButton type="radio" [value]="1">IPv6
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table class="table table-borderless">
|
||||
<thead>
|
||||
<th class="alias text-left" i18n="lightning.alias">Alias</th>
|
||||
<th class="text-left">Connect</th>
|
||||
<th class="city text-right d-none d-md-table-cell" i18n="lightning.location">Location</th>
|
||||
</thead>
|
||||
<tbody *ngIf="nodes$ | async as response; else skeleton">
|
||||
<tr *ngFor="let node of response.nodes; let i = index; trackBy: trackByPublicKey">
|
||||
<td class="alias text-left">
|
||||
<div class="text-truncate">
|
||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||
<div class="second-line">{{ node.opened_channel_count }} channel(s), <app-amount *ngIf="node.capacity > 100000000; else smallnode" [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||
<ng-template #smallnode>
|
||||
{{ node.capacity | amountShortener: 1 }} <span class="sats" i18n="shared.sats">sats</span>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="timestamp-first text-left">
|
||||
<div class="input-group" *ngIf="node.socketsObject.length">
|
||||
<ng-template #noDropdown>
|
||||
<span class="input-group-text" id="basic-addon3">{{ node.socketsObject[selectedSocketIndex].label }}</span>
|
||||
</ng-template>
|
||||
<input type="text" class="form-control" aria-label="Text input with dropdown button"
|
||||
[value]="node.socketsObject[selectedSocketIndex].socket">
|
||||
<button class="btn btn-secondary ml-1" type="button" id="inputGroupFileAddon04" (mouseover)="qrCodeVisible[i] = 1"
|
||||
(mouseout)="qrCodeVisible[i] = 0">
|
||||
<fa-icon [icon]="['fas', 'qrcode']" [fixedWidth]="true"></fa-icon>
|
||||
<div class="qr-wrapper" [hidden]="!qrCodeVisible[i]">
|
||||
<app-qrcode [size]="200" [data]="node.socketsObject[selectedSocketIndex].socket"></app-qrcode>
|
||||
</div>
|
||||
</button>
|
||||
<button class="btn btn-secondary ml-1" type="button" id="inputGroupFileAddon04">
|
||||
<app-clipboard [text]="node.socketsObject[selectedSocketIndex].socket" [leftPadding]="false"></app-clipboard>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="city text-right text-truncate d-none d-md-table-cell">
|
||||
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
|
||||
</td>
|
||||
</tbody>
|
||||
|
||||
<ng-template #skeleton>
|
||||
<tbody>
|
||||
<tr *ngFor="let item of skeletonLines">
|
||||
<td class="alias">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
<td class="timestamp-update d-none d-md-table-cell">
|
||||
<span class="skeleton-loader"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</ng-template>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
52
frontend/src/app/lightning/group/group.component.scss
Normal file
52
frontend/src/app/lightning/group/group.component.scss
Normal file
@@ -0,0 +1,52 @@
|
||||
.logo-container {
|
||||
width: 250px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
background-color: #FFF;
|
||||
padding: 10px;
|
||||
padding-bottom: 5px;
|
||||
display: inline-block;
|
||||
|
||||
|
||||
position: absolute;
|
||||
bottom: 50px;
|
||||
left: -175px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.dropdownLabel {
|
||||
min-width: 50px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#inputGroupFileAddon04 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toggle-holder {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.text-truncate {
|
||||
width: 120px;
|
||||
}
|
||||
.btn {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.second-line {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
103
frontend/src/app/lightning/group/group.component.ts
Normal file
103
frontend/src/app/lightning/group/group.component.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { map, Observable, share } from 'rxjs';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-group',
|
||||
templateUrl: './group.component.html',
|
||||
styleUrls: ['./group.component.scss']
|
||||
})
|
||||
export class GroupComponent implements OnInit {
|
||||
nodes$: Observable<any>;
|
||||
isp: {name: string, id: number};
|
||||
|
||||
skeletonLines: number[] = [];
|
||||
selectedSocketIndex = 0;
|
||||
qrCodeVisible = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
socketToggleForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private seoService: SeoService,
|
||||
private formBuilder: FormBuilder,
|
||||
) {
|
||||
for (let i = 0; i < 20; ++i) {
|
||||
this.skeletonLines.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.socketToggleForm = this.formBuilder.group({
|
||||
socket: [this.selectedSocketIndex],
|
||||
});
|
||||
|
||||
this.socketToggleForm.get('socket').valueChanges.subscribe((val) => {
|
||||
this.selectedSocketIndex = val;
|
||||
});
|
||||
|
||||
this.seoService.setTitle(`Mempool.space Lightning Nodes`);
|
||||
|
||||
this.nodes$ = this.lightningApiService.getNodGroupNodes$('mempool.space')
|
||||
.pipe(
|
||||
map((nodes) => {
|
||||
for (const node of nodes) {
|
||||
const socketsObject = [];
|
||||
for (const socket of node.sockets.split(',')) {
|
||||
if (socket === '') {
|
||||
continue;
|
||||
}
|
||||
let label = '';
|
||||
if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) {
|
||||
label = 'IPv4';
|
||||
} else if (socket.indexOf('[') > -1) {
|
||||
label = 'IPv6';
|
||||
} else if (socket.indexOf('onion') > -1) {
|
||||
label = 'Tor';
|
||||
}
|
||||
socketsObject.push({
|
||||
label: label,
|
||||
socket: node.public_key + '@' + socket,
|
||||
});
|
||||
}
|
||||
// @ts-ignore
|
||||
node.socketsObject = socketsObject;
|
||||
|
||||
if (!node?.country && !node?.city &&
|
||||
!node?.subdivision) {
|
||||
// @ts-ignore
|
||||
node.geolocation = null;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.geolocation = <GeolocationData>{
|
||||
country: node.country?.en,
|
||||
city: node.city?.en,
|
||||
subdivision: node.subdivision?.en,
|
||||
iso: node.iso_code,
|
||||
};
|
||||
}
|
||||
}
|
||||
const sumLiquidity = nodes.reduce((partialSum, a) => partialSum + parseInt(a.capacity, 10), 0);
|
||||
const sumChannels = nodes.reduce((partialSum, a) => partialSum + a.opened_channel_count, 0);
|
||||
|
||||
return {
|
||||
nodes: nodes,
|
||||
sumLiquidity: sumLiquidity,
|
||||
sumChannels: sumChannels,
|
||||
};
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
trackByPublicKey(index: number, node: any): string {
|
||||
return node.public_key;
|
||||
}
|
||||
|
||||
changeSocket(index: number) {
|
||||
this.selectedSocketIndex = index;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,6 +27,10 @@ export class LightningApiService {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||
}
|
||||
|
||||
getNodGroupNodes$(name: string): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
|
||||
}
|
||||
|
||||
getChannel$(shortId: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
|
||||
}
|
||||
|
||||
@@ -84,3 +84,10 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-small text-center mt-1" *ngIf="officialMempoolSpace">
|
||||
<a [routerLink]="['/lightning/group/mempool.space' | relativeUrl]">Connect to our nodes</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Observable } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { INodesRanking } from 'src/app/interfaces/node-api.interface';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
|
||||
@Component({
|
||||
@@ -14,10 +15,12 @@ import { LightningApiService } from '../lightning-api.service';
|
||||
export class LightningDashboardComponent implements OnInit {
|
||||
statistics$: Observable<any>;
|
||||
nodesRanking$: Observable<INodesRanking>;
|
||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
private seoService: SeoService,
|
||||
private stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { TopNodesPerCapacity } from '../lightning/nodes-ranking/top-nodes-per-ca
|
||||
import { OldestNodes } from '../lightning/nodes-ranking/oldest-nodes/oldest-nodes.component';
|
||||
import { NodesRankingsDashboard } from '../lightning/nodes-rankings-dashboard/nodes-rankings-dashboard.component';
|
||||
import { NodeChannels } from '../lightning/nodes-channels/node-channels.component';
|
||||
import { GroupComponent } from './group/group.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -58,6 +59,7 @@ import { NodeChannels } from '../lightning/nodes-channels/node-channels.componen
|
||||
OldestNodes,
|
||||
NodesRankingsDashboard,
|
||||
NodeChannels,
|
||||
GroupComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
import { GroupComponent } from './group/group.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -34,6 +35,10 @@ const routes: Routes = [
|
||||
path: 'nodes/isp/:isp',
|
||||
component: NodesPerISP,
|
||||
},
|
||||
{
|
||||
path: 'group/mempool.space',
|
||||
component: GroupComponent,
|
||||
},
|
||||
{
|
||||
path: 'nodes/rankings',
|
||||
component: NodesRankingsDashboard,
|
||||
|
||||
@@ -46,7 +46,9 @@ export class NodesMap implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning nodes world map`);
|
||||
if (!this.widget) {
|
||||
this.seoService.setTitle($localize`Lightning nodes world map`);
|
||||
}
|
||||
|
||||
if (!this.inputNodes$) {
|
||||
this.inputNodes$ = new BehaviorSubject(this.nodes);
|
||||
|
||||
Reference in New Issue
Block a user