mirror of
https://github.com/mempool/mempool.git
synced 2025-04-21 22:16:05 +02:00
Merge pull request #2157 from mempool/nymkappa/feature/ln-map-single-node
Add channels map to the node page
This commit is contained in:
commit
300b9e4e05
@ -13,9 +13,10 @@ class ChannelsApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getAllChannelsGeo(): Promise<any[]> {
|
||||
public async $getAllChannelsGeo(publicKey?: string): Promise<any[]> {
|
||||
try {
|
||||
const query = `SELECT nodes_1.public_key as node1_public_key, nodes_1.alias AS node1_alias,
|
||||
const params: string[] = [];
|
||||
let query = `SELECT nodes_1.public_key as node1_public_key, nodes_1.alias AS node1_alias,
|
||||
nodes_1.latitude AS node1_latitude, nodes_1.longitude AS node1_longitude,
|
||||
nodes_2.public_key as node2_public_key, nodes_2.alias AS node2_alias,
|
||||
nodes_2.latitude AS node2_latitude, nodes_2.longitude AS node2_longitude,
|
||||
@ -26,7 +27,14 @@ class ChannelsApi {
|
||||
WHERE nodes_1.latitude IS NOT NULL AND nodes_1.longitude IS NOT NULL
|
||||
AND nodes_2.latitude IS NOT NULL AND nodes_2.longitude IS NOT NULL
|
||||
`;
|
||||
const [rows]: any = await DB.query(query);
|
||||
|
||||
if (publicKey !== undefined) {
|
||||
query += ' AND (nodes_1.public_key = ? OR nodes_2.public_key = ?)';
|
||||
params.push(publicKey);
|
||||
params.push(publicKey);
|
||||
}
|
||||
|
||||
const [rows]: any = await DB.query(query, params);
|
||||
return rows.map((row) => [
|
||||
row.node1_public_key, row.node1_alias, row.node1_longitude, row.node1_latitude,
|
||||
row.node2_public_key, row.node2_alias, row.node2_longitude, row.node2_latitude,
|
||||
|
@ -11,7 +11,8 @@ class ChannelsRoutes {
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo', this.$getChannelsGeo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo', this.$getAllChannelsGeo)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels-geo/:publicKey', this.$getAllChannelsGeo)
|
||||
;
|
||||
}
|
||||
|
||||
@ -94,9 +95,9 @@ class ChannelsRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getChannelsGeo(req: Request, res: Response) {
|
||||
private async $getAllChannelsGeo(req: Request, res: Response) {
|
||||
try {
|
||||
const channels = await channelsApi.$getAllChannelsGeo();
|
||||
const channels = await channelsApi.$getAllChannelsGeo(req.params?.publicKey);
|
||||
res.json(channels);
|
||||
} catch (e) {
|
||||
res.status(500).send(e instanceof Error ? e.message : e);
|
||||
|
@ -1,6 +1,4 @@
|
||||
<div *ngIf="channels$ | async as response; else skeleton">
|
||||
<h2 class="float-left">Channels ({{ response.totalItems }})</h2>
|
||||
|
||||
<form [formGroup]="channelStatusForm" class="formRadioGroup float-right">
|
||||
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
|
||||
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||
|
@ -1,4 +1,4 @@
|
||||
<app-nodes-channels-map [widget]=true></app-nodes-channels-map>
|
||||
<app-nodes-channels-map [style]="'widget'"></app-nodes-channels-map>
|
||||
|
||||
<div class="container-xl dashboard-container">
|
||||
|
||||
|
@ -107,7 +107,20 @@
|
||||
|
||||
<br>
|
||||
|
||||
<app-channels-list [publicKey]="node.public_key"></app-channels-list>
|
||||
<div class="d-flex justify-content-between">
|
||||
<h2>Channels ({{ node.channel_count }})</h2>
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<span style="margin-bottom: 0.5rem">List</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" (change)="channelsListModeChange($event)">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<span style="margin-bottom: 0.5rem">Map</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-nodes-channels-map *ngIf="channelsListMode === 'map'" [style]="'nodepage'" [publicKey]="node.public_key"></app-nodes-channels-map>
|
||||
<app-channels-list *ngIf="channelsListMode === 'list'" [publicKey]="node.public_key"></app-channels-list>
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -58,3 +58,65 @@ app-fiat {
|
||||
}
|
||||
}
|
||||
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(13px);
|
||||
-ms-transform: translateX(13px);
|
||||
transform: translateX(13px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
@ -17,6 +17,7 @@ export class NodeComponent implements OnInit {
|
||||
publicKey$: Observable<string>;
|
||||
selectedSocketIndex = 0;
|
||||
qrCodeVisible = false;
|
||||
channelsListMode = 'list';
|
||||
|
||||
constructor(
|
||||
private lightningApiService: LightningApiService,
|
||||
@ -61,4 +62,11 @@ export class NodeComponent implements OnInit {
|
||||
this.selectedSocketIndex = index;
|
||||
}
|
||||
|
||||
channelsListModeChange(e) {
|
||||
if (e.target.checked === true) {
|
||||
this.channelsListMode = 'map';
|
||||
} else {
|
||||
this.channelsListMode = 'list';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div [class]="widget ? 'widget' : 'full-container'">
|
||||
<div [class]="'full-container ' + style">
|
||||
|
||||
<div class="card-header" *ngIf="!widget">
|
||||
<div *ngIf="style === 'graph'" class="card-header">
|
||||
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
|
||||
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
|
||||
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px">
|
||||
|
@ -19,6 +19,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.full-container.nodepage {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.full-container.widget {
|
||||
height: 250px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
width: 99vw;
|
||||
height: 250px;
|
||||
@ -30,6 +39,7 @@
|
||||
}
|
||||
|
||||
.chart {
|
||||
min-height: 500px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-right: 10px;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { Observable, tap, zip } from 'rxjs';
|
||||
import { Observable, switchMap, tap, zip } from 'rxjs';
|
||||
import { AssetsService } from 'src/app/services/assets.service';
|
||||
import { download } from 'src/app/shared/graphs.utils';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
||||
import { StateService } from 'src/app/services/state.service';
|
||||
import { EChartsOption, registerMap } from 'echarts';
|
||||
@ -17,7 +17,9 @@ import 'echarts-gl';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
@Input() widget = false;
|
||||
@Input() style: 'graph' | 'nodepage' | 'widget' = 'graph';
|
||||
@Input() publicKey: string | undefined;
|
||||
|
||||
observable$: Observable<any>;
|
||||
|
||||
chartInstance = undefined;
|
||||
@ -33,38 +35,46 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
private assetsService: AssetsService,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
||||
if (this.style === 'graph') {
|
||||
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
||||
}
|
||||
|
||||
this.observable$ = this.activatedRoute.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
return zip(
|
||||
this.assetsService.getWorldMapJson$,
|
||||
this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined),
|
||||
).pipe(tap((data) => {
|
||||
registerMap('world', data[0]);
|
||||
|
||||
this.observable$ = zip(
|
||||
this.assetsService.getWorldMapJson$,
|
||||
this.apiService.getChannelsGeo$(),
|
||||
).pipe(tap((data) => {
|
||||
registerMap('world', data[0]);
|
||||
const channelsLoc = [];
|
||||
const nodes = [];
|
||||
for (const channel of data[1]) {
|
||||
channelsLoc.push([[channel[2], channel[3]], [channel[6], channel[7]]]);
|
||||
nodes.push({
|
||||
publicKey: channel[0],
|
||||
name: channel[1],
|
||||
value: [channel[2], channel[3]],
|
||||
});
|
||||
nodes.push({
|
||||
publicKey: channel[4],
|
||||
name: channel[5],
|
||||
value: [channel[6], channel[7]],
|
||||
});
|
||||
}
|
||||
|
||||
const channelsLoc = [];
|
||||
const nodes = [];
|
||||
for (const channel of data[1]) {
|
||||
channelsLoc.push([[channel[2], channel[3]], [channel[6], channel[7]]]);
|
||||
nodes.push({
|
||||
publicKey: channel[0],
|
||||
name: channel[1],
|
||||
value: [channel[2], channel[3]],
|
||||
});
|
||||
nodes.push({
|
||||
publicKey: channel[4],
|
||||
name: channel[5],
|
||||
value: [channel[6], channel[7]],
|
||||
});
|
||||
}
|
||||
|
||||
this.prepareChartOptions(nodes, channelsLoc);
|
||||
}));
|
||||
this.prepareChartOptions(nodes, channelsLoc);
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
prepareChartOptions(nodes, channels) {
|
||||
@ -75,13 +85,14 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
color: 'grey',
|
||||
fontSize: 15
|
||||
},
|
||||
text: $localize`No data to display yet`,
|
||||
text: $localize`No geolocation data available`,
|
||||
left: 'center',
|
||||
top: 'center'
|
||||
};
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
title: title ?? undefined,
|
||||
geo3D: {
|
||||
map: 'world',
|
||||
shading: 'color',
|
||||
@ -89,16 +100,16 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
postEffect: {
|
||||
enable: true,
|
||||
bloom: {
|
||||
intensity: 0.01,
|
||||
intensity: this.style === 'nodepage' ? 0.1 : 0.01,
|
||||
}
|
||||
},
|
||||
viewControl: {
|
||||
center: this.widget ? [2, 0, -10] : undefined,
|
||||
center: this.style === 'widget' ? [0, 0, -1] : undefined,
|
||||
minDistance: 0.1,
|
||||
distance: this.widget ? 20 : 60,
|
||||
distance: this.style === 'widget' ? 45 : 60,
|
||||
alpha: 90,
|
||||
panMouseButton: 'left',
|
||||
rotateMouseButton: 'none',
|
||||
rotateMouseButton: undefined,
|
||||
zoomSensivity: 0.5,
|
||||
},
|
||||
itemStyle: {
|
||||
@ -117,7 +128,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
||||
blendMode: 'lighter',
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
opacity: 0.025,
|
||||
opacity: ['widget', 'graph'].includes(this.style) ? 0.025 : 1,
|
||||
},
|
||||
data: channels
|
||||
},
|
||||
|
@ -271,7 +271,10 @@ export class ApiService {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries');
|
||||
}
|
||||
|
||||
getChannelsGeo$(): Observable<any> {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo');
|
||||
getChannelsGeo$(publicKey?: string): Observable<any> {
|
||||
return this.httpClient.get<any[]>(
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo' +
|
||||
(publicKey !== undefined ? `/${publicKey}` : '')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user