Add channels map to the node page

This commit is contained in:
nymkappa 2022-07-23 15:43:38 +02:00
parent 33776b2b09
commit 40f2b97075
No known key found for this signature in database
GPG Key ID: E155910B16E8BD04
10 changed files with 151 additions and 42 deletions

View File

@ -13,9 +13,10 @@ class ChannelsApi {
} }
} }
public async $getAllChannelsGeo(): Promise<any[]> { public async $getAllChannelsGeo(publicKey?: string): Promise<any[]> {
try { 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_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.public_key as node2_public_key, nodes_2.alias AS node2_alias,
nodes_2.latitude AS node2_latitude, nodes_2.longitude AS node2_longitude, 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 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 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) => [ return rows.map((row) => [
row.node1_public_key, row.node1_alias, row.node1_longitude, row.node1_latitude, 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, row.node2_public_key, row.node2_alias, row.node2_longitude, row.node2_latitude,

View File

@ -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/search/:search', this.$searchChannelsById)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel) .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', 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 { try {
const channels = await channelsApi.$getAllChannelsGeo(); const channels = await channelsApi.$getAllChannelsGeo(req.params?.publicKey);
res.json(channels); res.json(channels);
} catch (e) { } catch (e) {
res.status(500).send(e instanceof Error ? e.message : e); res.status(500).send(e instanceof Error ? e.message : e);

View File

@ -1,6 +1,4 @@
<div *ngIf="channels$ | async as response; else skeleton"> <div *ngIf="channels$ | async as response; else skeleton">
<h2 class="float-left">Channels ({{ response.totalItems }})</h2>
<form [formGroup]="channelStatusForm" class="formRadioGroup float-right"> <form [formGroup]="channelStatusForm" class="formRadioGroup float-right">
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status"> <div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
<label ngbButtonLabel class="btn-primary btn-sm"> <label ngbButtonLabel class="btn-primary btn-sm">

View File

@ -107,7 +107,20 @@
<br> <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>&nbsp;
<label class="switch">
<input type="checkbox" (change)="channelsListModeChange($event)">
<span class="slider round"></span>
</label>
&nbsp;<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> </div>

View File

@ -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%;
}

View File

@ -17,6 +17,7 @@ export class NodeComponent implements OnInit {
publicKey$: Observable<string>; publicKey$: Observable<string>;
selectedSocketIndex = 0; selectedSocketIndex = 0;
qrCodeVisible = false; qrCodeVisible = false;
channelsListMode = 'list';
constructor( constructor(
private lightningApiService: LightningApiService, private lightningApiService: LightningApiService,
@ -61,4 +62,11 @@ export class NodeComponent implements OnInit {
this.selectedSocketIndex = index; this.selectedSocketIndex = index;
} }
channelsListModeChange(e) {
if (e.target.checked === true) {
this.channelsListMode = 'map';
} else {
this.channelsListMode = 'list';
}
}
} }

View File

@ -1,6 +1,6 @@
<div [class]="widget ? 'widget' : 'full-container'"> <div [class]="style === 'graph' ? '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"> <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> <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"> <button class="btn p-0 pl-2" style="margin: 0 0 4px 0px">

View File

@ -29,7 +29,12 @@
-webkit-mask: linear-gradient(180deg, #11131f00 0%, #11131fff 20%); -webkit-mask: linear-gradient(180deg, #11131f00 0%, #11131fff 20%);
} }
.full-container.nodepage {
margin-top: 50px;
}
.chart { .chart {
min-height: 500px;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding-right: 10px; padding-right: 10px;

View File

@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { ApiService } from 'src/app/services/api.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 { AssetsService } from 'src/app/services/assets.service';
import { download } from 'src/app/shared/graphs.utils'; 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 { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from 'src/app/services/state.service'; import { StateService } from 'src/app/services/state.service';
import { EChartsOption, registerMap } from 'echarts'; import { EChartsOption, registerMap } from 'echarts';
@ -17,7 +17,9 @@ import 'echarts-gl';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NodesChannelsMap implements OnInit, OnDestroy { export class NodesChannelsMap implements OnInit, OnDestroy {
@Input() widget = false; @Input() style: 'graph' | 'nodepage' | 'widget' = 'graph';
@Input() publicKey: string | undefined;
observable$: Observable<any>; observable$: Observable<any>;
chartInstance = undefined; chartInstance = undefined;
@ -33,38 +35,46 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
private assetsService: AssetsService, private assetsService: AssetsService,
private router: Router, private router: Router,
private zone: NgZone, private zone: NgZone,
private activatedRoute: ActivatedRoute,
) { ) {
} }
ngOnDestroy(): void {} ngOnDestroy(): void {}
ngOnInit(): 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')),
).pipe(tap((data) => {
registerMap('world', data[0]);
this.observable$ = zip( const channelsLoc = [];
this.assetsService.getWorldMapJson$, const nodes = [];
this.apiService.getChannelsGeo$(), for (const channel of data[1]) {
).pipe(tap((data) => { channelsLoc.push([[channel[2], channel[3]], [channel[6], channel[7]]]);
registerMap('world', data[0]); 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 = []; this.prepareChartOptions(nodes, 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);
}));
} }
prepareChartOptions(nodes, channels) { prepareChartOptions(nodes, channels) {
@ -75,13 +85,14 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
color: 'grey', color: 'grey',
fontSize: 15 fontSize: 15
}, },
text: $localize`No data to display yet`, text: $localize`No geolocation data available`,
left: 'center', left: 'center',
top: 'center' top: 'center'
}; };
} }
this.chartOptions = { this.chartOptions = {
title: title ?? undefined,
geo3D: { geo3D: {
map: 'world', map: 'world',
shading: 'color', shading: 'color',
@ -117,7 +128,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
blendMode: 'lighter', blendMode: 'lighter',
lineStyle: { lineStyle: {
width: 1, width: 1,
opacity: 0.025, opacity: this.style === 'graph' ? 0.025 : 1,
}, },
data: channels data: channels
}, },

View File

@ -271,7 +271,10 @@ export class ApiService {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries'); return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries');
} }
getChannelsGeo$(): Observable<any> { getChannelsGeo$(publicKey?: string): Observable<any> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo'); return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo' +
(publicKey !== undefined ? `/${publicKey}` : '')
);
} }
} }