Show summary stats and world map in isp and country node list page
This commit is contained in:
		
							parent
							
								
									004768132b
								
							
						
					
					
						commit
						dcfcac2cc6
					
				@ -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,
 | 
			
		||||
          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_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
 | 
			
		||||
        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_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_isp on geo_names_isp.id = nodes.as_number AND geo_names_isp.type = 'as_organization'
 | 
			
		||||
        WHERE geo_names_country.id = ?
 | 
			
		||||
        ORDER BY capacity DESC
 | 
			
		||||
      `;
 | 
			
		||||
@ -449,6 +451,7 @@ class NodesApi {
 | 
			
		||||
        rows[i].country = JSON.parse(rows[i].country);
 | 
			
		||||
        rows[i].city = JSON.parse(rows[i].city);
 | 
			
		||||
        rows[i].subdivision = JSON.parse(rows[i].subdivision);
 | 
			
		||||
        rows[i].isp = JSON.parse(rows[i].isp);
 | 
			
		||||
      }
 | 
			
		||||
      return rows;
 | 
			
		||||
    } 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,
 | 
			
		||||
          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_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
 | 
			
		||||
        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'
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,8 @@
 | 
			
		||||
 | 
			
		||||
  <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">
 | 
			
		||||
 | 
			
		||||
@ -51,38 +52,57 @@
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <br>
 | 
			
		||||
 | 
			
		||||
    <div class="row row-cols-1 row-cols-md-2">
 | 
			
		||||
      <div class="col">
 | 
			
		||||
        <app-channel-box [channel]="channel.node_left"></app-channel-box>
 | 
			
		||||
      <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 class="col">
 | 
			
		||||
        <app-channel-box [channel]="channel.node_right"></app-channel-box>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <br>
 | 
			
		||||
 | 
			
		||||
    <ng-container *ngIf="transactions$ | async as transactions">
 | 
			
		||||
      <ng-template [ngIf]="transactions[0]">
 | 
			
		||||
        <div class="d-flex">
 | 
			
		||||
          <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>
 | 
			
		||||
        </div>
 | 
			
		||||
        <app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
      <ng-template [ngIf]="transactions[1]">
 | 
			
		||||
        <div class="closing-header d-flex">
 | 
			
		||||
          <h3 style="margin: 0;">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason"></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>
 | 
			
		||||
        <app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <div class="row row-cols-1 row-cols-md-2">
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <app-channel-box [channel]="channel.node_left"></app-channel-box>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col">
 | 
			
		||||
      <app-channel-box [channel]="channel.node_right"></app-channel-box>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <ng-container *ngIf="transactions$ | async as transactions">
 | 
			
		||||
    <ng-template [ngIf]="transactions[0]">
 | 
			
		||||
      <div class="d-flex">
 | 
			
		||||
        <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>
 | 
			
		||||
      </div>
 | 
			
		||||
      <app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5">
 | 
			
		||||
      </app-transactions-list>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
    <ng-template [ngIf]="transactions[1]">
 | 
			
		||||
      <div class="closing-header d-flex">
 | 
			
		||||
        <h3 style="margin: 0;">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason">
 | 
			
		||||
        </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>
 | 
			
		||||
      <app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5">
 | 
			
		||||
      </app-transactions-list>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </ng-container>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -108,7 +128,7 @@
 | 
			
		||||
    <div class="badges mb-2">
 | 
			
		||||
      <span class="skeleton-loader" style="width: 50px; height: 22px; margin-top: 5px;"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
    <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
    <div style="height: 413px;  padding: 15px;">
 | 
			
		||||
@ -152,4 +172,4 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
@ -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">
 | 
			
		||||
      <span i18n="lightning.nodes-world-map">Lightning nodes world map</span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
 | 
			
		||||
  </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)">
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,11 @@
 | 
			
		||||
    padding-bottom: 100px;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
.full-container.widget {
 | 
			
		||||
  min-height: 240px;
 | 
			
		||||
  height: 240px;
 | 
			
		||||
  padding: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chart {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
@ -38,3 +43,6 @@
 | 
			
		||||
    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 { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { Observable, tap, zip } from 'rxjs';
 | 
			
		||||
@ -18,6 +18,10 @@ import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodesMap implements OnInit {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
  @Input() nodes: any[] | undefined = undefined;
 | 
			
		||||
  @Input() type: 'none' | 'isp' | 'country' = 'none';
 | 
			
		||||
  
 | 
			
		||||
  observable$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  chartInstance = undefined;
 | 
			
		||||
@ -43,13 +47,48 @@ export class NodesMap implements OnInit {
 | 
			
		||||
 | 
			
		||||
    this.observable$ = zip(
 | 
			
		||||
      this.assetsService.getWorldMapJson$,
 | 
			
		||||
      this.apiService.getWorldNodes$()
 | 
			
		||||
      this.nodes ? [this.nodes] : this.apiService.getWorldNodes$()
 | 
			
		||||
    ).pipe(tap((data) => {
 | 
			
		||||
      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[] = [];
 | 
			
		||||
      console.log(data[1].nodes[0]);
 | 
			
		||||
      for (const node of data[1].nodes) {
 | 
			
		||||
      for (const node of inputNodes) {
 | 
			
		||||
        // We add a bit of noise so nodes at the same location are not all
 | 
			
		||||
        // on top of each other
 | 
			
		||||
        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;
 | 
			
		||||
    if (nodes.length === 0) {
 | 
			
		||||
      title = {
 | 
			
		||||
@ -91,8 +131,8 @@ export class NodesMap implements OnInit {
 | 
			
		||||
      geo: {
 | 
			
		||||
        animation: false,
 | 
			
		||||
        silent: true,
 | 
			
		||||
        center: [0, 5],
 | 
			
		||||
        zoom: 1.3,
 | 
			
		||||
        center: mapCenter,
 | 
			
		||||
        zoom: mapZoom,
 | 
			
		||||
        tooltip: {
 | 
			
		||||
          show: false
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,58 @@
 | 
			
		||||
<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 style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
 | 
			
		||||
  </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">
 | 
			
		||||
    <table class="table table-borderless">
 | 
			
		||||
      
 | 
			
		||||
@ -15,9 +64,8 @@
 | 
			
		||||
        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
			
		||||
        <th class="city text-right" i18n="lightning.location">Location</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
      
 | 
			
		||||
      <tbody *ngIf="nodes$ | async as nodes; else skeleton">
 | 
			
		||||
        <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
 | 
			
		||||
      <tbody *ngIf="nodes$ | async as countryNodes; else skeleton">
 | 
			
		||||
        <tr *ngFor="let node of countryNodes.nodes; let i= index; trackBy: trackByPublicKey">
 | 
			
		||||
          <td class="alias text-left text-truncate">
 | 
			
		||||
            <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
 | 
			
		||||
          </td>
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
			
		||||
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 { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
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)
 | 
			
		||||
      .pipe(
 | 
			
		||||
        map(response => {
 | 
			
		||||
          this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
 | 
			
		||||
 | 
			
		||||
          this.country = {
 | 
			
		||||
            name: response.country.en,
 | 
			
		||||
            flag: getFlagEmoji(this.route.snapshot.params.country)
 | 
			
		||||
@ -45,14 +47,50 @@ export class NodesPerCountry implements OnInit {
 | 
			
		||||
              iso: response.nodes[i].iso_code,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
 | 
			
		||||
          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 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,54 @@
 | 
			
		||||
<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">
 | 
			
		||||
    <table class="table table-borderless">
 | 
			
		||||
@ -12,9 +61,8 @@
 | 
			
		||||
        <th class="channels text-right" i18n="lightning.channels">Channels</th>
 | 
			
		||||
        <th class="city text-right" i18n="lightning.location">Location</th>
 | 
			
		||||
      </thead>
 | 
			
		||||
 | 
			
		||||
      <tbody *ngIf="nodes$ | async as nodes; else skeleton">
 | 
			
		||||
        <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
 | 
			
		||||
      <tbody *ngIf="nodes$ | async as ispNodes; else skeleton">
 | 
			
		||||
        <tr *ngFor="let node of ispNodes.nodes; let i= index; trackBy: trackByPublicKey">
 | 
			
		||||
          <td class="alias text-left text-truncate">
 | 
			
		||||
            <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
 | 
			
		||||
          </td>
 | 
			
		||||
 | 
			
		||||
@ -59,4 +59,4 @@
 | 
			
		||||
  @media (max-width: 576px) {
 | 
			
		||||
    display: none
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
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 { 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';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -33,7 +34,7 @@ export class NodesPerISP implements OnInit {
 | 
			
		||||
        map(response => {
 | 
			
		||||
          this.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}]`);
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user