Merge pull request #2527 from mempool/nymkappa/feature/isp-country-map-stats
ISP and Country node lists header
This commit is contained in:
commit
2aeccd72e9
@ -434,12 +434,14 @@ class NodesApi {
|
|||||||
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
||||||
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||||
geo_names_city.names as city, geo_names_country.names as country,
|
geo_names_city.names as city, geo_names_country.names as country,
|
||||||
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision,
|
||||||
|
nodes.longitude, nodes.latitude, nodes.as_number, geo_names_isp.names as isp
|
||||||
FROM nodes
|
FROM nodes
|
||||||
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_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'
|
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||||
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
|
||||||
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
|
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
|
||||||
|
LEFT JOIN geo_names geo_names_isp on geo_names_isp.id = nodes.as_number AND geo_names_isp.type = 'as_organization'
|
||||||
WHERE geo_names_country.id = ?
|
WHERE geo_names_country.id = ?
|
||||||
ORDER BY capacity DESC
|
ORDER BY capacity DESC
|
||||||
`;
|
`;
|
||||||
@ -449,6 +451,7 @@ class NodesApi {
|
|||||||
rows[i].country = JSON.parse(rows[i].country);
|
rows[i].country = JSON.parse(rows[i].country);
|
||||||
rows[i].city = JSON.parse(rows[i].city);
|
rows[i].city = JSON.parse(rows[i].city);
|
||||||
rows[i].subdivision = JSON.parse(rows[i].subdivision);
|
rows[i].subdivision = JSON.parse(rows[i].subdivision);
|
||||||
|
rows[i].isp = JSON.parse(rows[i].isp);
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -463,7 +466,8 @@ class NodesApi {
|
|||||||
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
|
||||||
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
|
||||||
geo_names_city.names as city, geo_names_country.names as country,
|
geo_names_city.names as city, geo_names_country.names as country,
|
||||||
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
|
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision,
|
||||||
|
nodes.longitude, nodes.latitude
|
||||||
FROM nodes
|
FROM nodes
|
||||||
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_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'
|
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
|
<app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'"
|
||||||
|
[channel]="channelGeo"></app-nodes-channels-map>
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
|
||||||
@ -51,6 +52,20 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-100 d-block d-md-none"></div>
|
||||||
|
<div class="col-md">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td i18n="address.total-received">Capacity</td>
|
||||||
|
<td>
|
||||||
|
<app-sats [satoshis]="channel.capacity"></app-sats>
|
||||||
|
<app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -71,16 +86,21 @@
|
|||||||
<ng-template [ngIf]="transactions[0]">
|
<ng-template [ngIf]="transactions[0]">
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<h3>Opening transaction</h3>
|
<h3>Opening transaction</h3>
|
||||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()"
|
||||||
|
i18n="transaction.details|Transaction Details">Details</button>
|
||||||
</div>
|
</div>
|
||||||
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
<app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5">
|
||||||
|
</app-transactions-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template [ngIf]="transactions[1]">
|
<ng-template [ngIf]="transactions[1]">
|
||||||
<div class="closing-header d-flex">
|
<div class="closing-header d-flex">
|
||||||
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason"></app-closing-type>
|
<h3 style="margin: 0;">Closing transaction</h3> <app-closing-type [type]="channel.closing_reason">
|
||||||
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
|
</app-closing-type>
|
||||||
|
<button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()"
|
||||||
|
i18n="transaction.details|Transaction Details">Details</button>
|
||||||
</div>
|
</div>
|
||||||
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
|
<app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5">
|
||||||
|
</app-transactions-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<div class="full-container">
|
<div class="full-container" [class]="widget ? 'widget' : ''">
|
||||||
|
|
||||||
<div class="card-header">
|
<div *ngIf="!widget" 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-world-map">Lightning nodes world map</span>
|
<span i18n="lightning.nodes-world-map">Lightning nodes world map</span>
|
||||||
</div>
|
</div>
|
||||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
<div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||||
(chartInit)="onChartInit($event)">
|
(chartInit)="onChartInit($event)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -16,6 +16,11 @@
|
|||||||
padding-bottom: 100px;
|
padding-bottom: 100px;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
.full-container.widget {
|
||||||
|
min-height: 240px;
|
||||||
|
height: 240px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.chart {
|
.chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -38,3 +43,6 @@
|
|||||||
padding-bottom: 55px;
|
padding-bottom: 55px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.chart.widget {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, 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, tap, zip } from 'rxjs';
|
||||||
@ -18,6 +18,10 @@ import { getFlagEmoji } from 'src/app/shared/common.utils';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class NodesMap implements OnInit {
|
export class NodesMap implements OnInit {
|
||||||
|
@Input() widget: boolean = false;
|
||||||
|
@Input() nodes: any[] | undefined = undefined;
|
||||||
|
@Input() type: 'none' | 'isp' | 'country' = 'none';
|
||||||
|
|
||||||
observable$: Observable<any>;
|
observable$: Observable<any>;
|
||||||
|
|
||||||
chartInstance = undefined;
|
chartInstance = undefined;
|
||||||
@ -43,13 +47,48 @@ export class NodesMap implements OnInit {
|
|||||||
|
|
||||||
this.observable$ = zip(
|
this.observable$ = zip(
|
||||||
this.assetsService.getWorldMapJson$,
|
this.assetsService.getWorldMapJson$,
|
||||||
this.apiService.getWorldNodes$()
|
this.nodes ? [this.nodes] : this.apiService.getWorldNodes$()
|
||||||
).pipe(tap((data) => {
|
).pipe(tap((data) => {
|
||||||
registerMap('world', data[0]);
|
registerMap('world', data[0]);
|
||||||
|
|
||||||
|
let maxLiquidity = data[1].maxLiquidity;
|
||||||
|
let inputNodes: any[] = data[1].nodes;
|
||||||
|
let mapCenter: number[] = [0, 5];
|
||||||
|
if (this.type === 'country') {
|
||||||
|
mapCenter = [0, 0];
|
||||||
|
} else if (this.type === 'isp') {
|
||||||
|
mapCenter = [0, 10];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mapZoom = 1.3;
|
||||||
|
if (!inputNodes) {
|
||||||
|
inputNodes = [];
|
||||||
|
for (const node of data[1]) {
|
||||||
|
if (this.type === 'country') {
|
||||||
|
mapCenter[0] += node.longitude;
|
||||||
|
mapCenter[1] += node.latitude;
|
||||||
|
}
|
||||||
|
inputNodes.push([
|
||||||
|
node.longitude,
|
||||||
|
node.latitude,
|
||||||
|
node.public_key,
|
||||||
|
node.alias,
|
||||||
|
node.capacity,
|
||||||
|
node.channels,
|
||||||
|
node.country,
|
||||||
|
node.iso_code,
|
||||||
|
]);
|
||||||
|
maxLiquidity = Math.max(maxLiquidity ?? 0, node.capacity);
|
||||||
|
}
|
||||||
|
if (this.type === 'country') {
|
||||||
|
mapCenter[0] /= data[1].length;
|
||||||
|
mapCenter[1] /= data[1].length;
|
||||||
|
mapZoom = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nodes: any[] = [];
|
const nodes: any[] = [];
|
||||||
console.log(data[1].nodes[0]);
|
for (const node of inputNodes) {
|
||||||
for (const node of data[1].nodes) {
|
|
||||||
// We add a bit of noise so nodes at the same location are not all
|
// We add a bit of noise so nodes at the same location are not all
|
||||||
// on top of each other
|
// on top of each other
|
||||||
const random = Math.random() * 2 * Math.PI;
|
const random = Math.random() * 2 * Math.PI;
|
||||||
@ -66,11 +105,12 @@ export class NodesMap implements OnInit {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.prepareChartOptions(nodes, data[1].maxLiquidity);
|
maxLiquidity = Math.max(1, maxLiquidity);
|
||||||
|
this.prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareChartOptions(nodes, maxLiquidity) {
|
prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom) {
|
||||||
let title: object;
|
let title: object;
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
title = {
|
title = {
|
||||||
@ -91,8 +131,8 @@ export class NodesMap implements OnInit {
|
|||||||
geo: {
|
geo: {
|
||||||
animation: false,
|
animation: false,
|
||||||
silent: true,
|
silent: true,
|
||||||
center: [0, 5],
|
center: mapCenter,
|
||||||
zoom: 1.3,
|
zoom: mapZoom,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
show: false
|
show: false
|
||||||
},
|
},
|
||||||
@ -122,10 +162,13 @@ export class NodesMap implements OnInit {
|
|||||||
return 10 * Math.pow(params[2] / maxLiquidity, 0.2) + 3;
|
return 10 * Math.pow(params[2] / maxLiquidity, 0.2) + 3;
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
position: function(point, params, dom, rect, size) {
|
||||||
|
return point;
|
||||||
|
},
|
||||||
trigger: 'item',
|
trigger: 'item',
|
||||||
show: true,
|
show: true,
|
||||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||||
borderRadius: 4,
|
borderRadius: 0,
|
||||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color: '#b1b1b1',
|
color: '#b1b1b1',
|
||||||
@ -155,7 +198,6 @@ export class NodesMap implements OnInit {
|
|||||||
borderColor: 'black',
|
borderColor: 'black',
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
},
|
},
|
||||||
blendMode: 'lighter',
|
|
||||||
zlevel: 2,
|
zlevel: 2,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -1,9 +1,58 @@
|
|||||||
<div class="container-xl full-height" style="min-height: 335px">
|
<div class="container-xl full-height" style="min-height: 335px">
|
||||||
<h1 class="float-left" i18n="lightning.nodes-in-country">
|
<h1 i18n="lightning.nodes-in-country">
|
||||||
<span>Lightning nodes in {{ country?.name }}</span>
|
<span>Lightning nodes in {{ country?.name }}</span>
|
||||||
<span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
|
<span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="row" *ngIf="nodes$ | async as countryNodes">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.node-count">Nodes</td>
|
||||||
|
<td>{{ countryNodes.nodes.length }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.liquidity">Liquidity</td>
|
||||||
|
<td>
|
||||||
|
<app-amount *ngIf="countryNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="countryNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
|
||||||
|
<ng-template #smallnode>
|
||||||
|
{{ countryNodes.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]="countryNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.channels">Channels</td>
|
||||||
|
<td>{{ countryNodes.sumChannels }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.isp-count">ISP Count</td>
|
||||||
|
<td>{{ countryNodes.ispCount }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.top-isp">Top ISP</td>
|
||||||
|
<td class="text-truncate">
|
||||||
|
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/isp' | relativeUrl, countryNodes.topIsp.id]">
|
||||||
|
{{ countryNodes.topIsp.name }} [ASN {{ countryNodes.topIsp.id }}]
|
||||||
|
</a>
|
||||||
|
</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]="countryNodes.nodes" type="country"></app-nodes-map>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="min-height: 295px">
|
<div style="min-height: 295px">
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
|
|
||||||
@ -15,9 +64,8 @@
|
|||||||
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||||
<th class="city text-right" i18n="lightning.location">Location</th>
|
<th class="city text-right" i18n="lightning.location">Location</th>
|
||||||
</thead>
|
</thead>
|
||||||
|
<tbody *ngIf="nodes$ | async as countryNodes; else skeleton">
|
||||||
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
|
<tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey">
|
||||||
<tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
|
|
||||||
<td class="alias text-left text-truncate">
|
<td class="alias text-left text-truncate">
|
||||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { map, Observable } from 'rxjs';
|
import { map, Observable, share } from 'rxjs';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||||
@ -32,6 +32,8 @@ export class NodesPerCountry implements OnInit {
|
|||||||
this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
|
this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)
|
||||||
.pipe(
|
.pipe(
|
||||||
map(response => {
|
map(response => {
|
||||||
|
this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
|
||||||
|
|
||||||
this.country = {
|
this.country = {
|
||||||
name: response.country.en,
|
name: response.country.en,
|
||||||
flag: getFlagEmoji(this.route.snapshot.params.country)
|
flag: getFlagEmoji(this.route.snapshot.params.country)
|
||||||
@ -46,13 +48,49 @@ export class NodesPerCountry implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
|
const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
|
||||||
return response.nodes;
|
const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
|
||||||
})
|
const isps = {};
|
||||||
|
const topIsp = {
|
||||||
|
count: 0,
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
for (const node of response.nodes) {
|
||||||
|
if (!node.isp) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isps[node.isp]) {
|
||||||
|
isps[node.isp] = {
|
||||||
|
count: 0,
|
||||||
|
asns: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (isps[node.isp].asns.indexOf(node.as_number) === -1) {
|
||||||
|
isps[node.isp].asns.push(node.as_number);
|
||||||
|
}
|
||||||
|
isps[node.isp].count++;
|
||||||
|
|
||||||
|
if (isps[node.isp].count > topIsp.count) {
|
||||||
|
topIsp.count = isps[node.isp].count;
|
||||||
|
topIsp.id = isps[node.isp].asns.join(',');
|
||||||
|
topIsp.name = node.isp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: response.nodes,
|
||||||
|
sumLiquidity: sumLiquidity,
|
||||||
|
sumChannels: sumChannels,
|
||||||
|
topIsp: topIsp,
|
||||||
|
ispCount: Object.keys(isps).length
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
share()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByPublicKey(index: number, node: any) {
|
trackByPublicKey(index: number, node: any): string {
|
||||||
return node.public_key;
|
return node.public_key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,54 @@
|
|||||||
<div class="container-xl full-height" style="min-height: 335px">
|
<div class="container-xl full-height" style="min-height: 335px">
|
||||||
<h1 class="float-left" i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }} [AS {{isp?.id}}]</h1>
|
<h1 i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }}</h1>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<div class="row" *ngIf="nodes$ | async as ispNodes">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.asn">ASN</td>
|
||||||
|
<td>{{ isp?.id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.node-count">Nodes</td>
|
||||||
|
<td>{{ ispNodes.nodes.length }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.liquidity">Liquidity</td>
|
||||||
|
<td>
|
||||||
|
<app-amount *ngIf="ispNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="ispNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
|
||||||
|
<ng-template #smallnode>
|
||||||
|
{{ ispNodes.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]="ispNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.channels">Channels</td>
|
||||||
|
<td>{{ ispNodes.sumChannels }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.top-country">Top country</td>
|
||||||
|
<td class="text-truncate">
|
||||||
|
<a class="d-block text-wrap" [routerLink]="['/lightning/nodes/country' | relativeUrl, ispNodes.topCountry.iso]">
|
||||||
|
<span class="">{{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }}</span>
|
||||||
|
</a>
|
||||||
|
</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]="ispNodes.nodes" type="isp"></app-nodes-map>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="min-height: 295px">
|
<div style="min-height: 295px">
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
@ -12,9 +61,8 @@
|
|||||||
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
<th class="channels text-right" i18n="lightning.channels">Channels</th>
|
||||||
<th class="city text-right" i18n="lightning.location">Location</th>
|
<th class="city text-right" i18n="lightning.location">Location</th>
|
||||||
</thead>
|
</thead>
|
||||||
|
<tbody *ngIf="nodes$ | async as ispNodes; else skeleton">
|
||||||
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
|
<tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey">
|
||||||
<tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
|
|
||||||
<td class="alias text-left text-truncate">
|
<td class="alias text-left text-truncate">
|
||||||
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -1,8 +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 { map, Observable } from 'rxjs';
|
import { map, Observable, share } from 'rxjs';
|
||||||
import { ApiService } from 'src/app/services/api.service';
|
import { ApiService } from 'src/app/services/api.service';
|
||||||
import { SeoService } from 'src/app/services/seo.service';
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
|
import { getFlagEmoji } from 'src/app/shared/common.utils';
|
||||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -33,7 +34,7 @@ export class NodesPerISP implements OnInit {
|
|||||||
map(response => {
|
map(response => {
|
||||||
this.isp = {
|
this.isp = {
|
||||||
name: response.isp,
|
name: response.isp,
|
||||||
id: this.route.snapshot.params.isp
|
id: this.route.snapshot.params.isp.split(',').join(', ')
|
||||||
};
|
};
|
||||||
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
|
||||||
|
|
||||||
@ -46,12 +47,40 @@ export class NodesPerISP implements OnInit {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.nodes;
|
const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
|
||||||
})
|
const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
|
||||||
|
const countries = {};
|
||||||
|
const topCountry = {
|
||||||
|
count: 0,
|
||||||
|
country: '',
|
||||||
|
iso: '',
|
||||||
|
flag: '',
|
||||||
|
};
|
||||||
|
for (const node of response.nodes) {
|
||||||
|
if (!node.geolocation.iso) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
countries[node.geolocation.iso] = countries[node.geolocation.iso] ?? 0 + 1;
|
||||||
|
if (countries[node.geolocation.iso] > topCountry.count) {
|
||||||
|
topCountry.count = countries[node.geolocation.iso];
|
||||||
|
topCountry.country = node.geolocation.country;
|
||||||
|
topCountry.iso = node.geolocation.iso;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
topCountry.flag = getFlagEmoji(topCountry.iso);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes: response.nodes,
|
||||||
|
sumLiquidity: sumLiquidity,
|
||||||
|
sumChannels: sumChannels,
|
||||||
|
topCountry: topCountry,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
share()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByPublicKey(index: number, node: any) {
|
trackByPublicKey(index: number, node: any): string {
|
||||||
return node.public_key;
|
return node.public_key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user