Add lighting ISP preview
This commit is contained in:
		
							parent
							
								
									9a87b357fc
								
							
						
					
					
						commit
						7d367572dc
					
				@ -8,10 +8,12 @@ import { LightningApiService } from './lightning-api.service';
 | 
			
		||||
import { NodePreviewComponent } from './node/node-preview.component';
 | 
			
		||||
import { LightningPreviewsRoutingModule } from './lightning-previews.routing.module';
 | 
			
		||||
import { ChannelPreviewComponent } from './channel/channel-preview.component';
 | 
			
		||||
import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component';
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    NodePreviewComponent,
 | 
			
		||||
    ChannelPreviewComponent,
 | 
			
		||||
    NodesPerISPPreview,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
 | 
			
		||||
import { RouterModule, Routes } from '@angular/router';
 | 
			
		||||
import { NodePreviewComponent } from './node/node-preview.component';
 | 
			
		||||
import { ChannelPreviewComponent } from './channel/channel-preview.component';
 | 
			
		||||
import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
    {
 | 
			
		||||
@ -12,6 +13,10 @@ const routes: Routes = [
 | 
			
		||||
      path: 'channel/:short_id',
 | 
			
		||||
      component: ChannelPreviewComponent,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'nodes/isp/:isp',
 | 
			
		||||
      component: NodesPerISPPreview,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '**',
 | 
			
		||||
      redirectTo: ''
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
<div class="full-container" [class]="widget ? 'widget' : ''">
 | 
			
		||||
<div class="full-container" [class]="widget ? 'widget' : ''" [class.fit-container]="fitContainer">
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="!widget" class="card-header">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
 | 
			
		||||
    (chartInit)="onChartInit($event)">
 | 
			
		||||
    (chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,17 @@
 | 
			
		||||
  height: 240px;
 | 
			
		||||
  padding: 0px;
 | 
			
		||||
}
 | 
			
		||||
.full-container.fit-container {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  min-height: 100px;
 | 
			
		||||
 | 
			
		||||
  .chart {
 | 
			
		||||
    padding: 0;
 | 
			
		||||
    min-height: 100px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chart {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Inject, Input, Output, EventEmitter, LOCALE_ID, NgZone, OnDestroy, OnInit, OnChanges } 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, BehaviorSubject, switchMap, tap, combineLatest } from 'rxjs';
 | 
			
		||||
import { AssetsService } from 'src/app/services/assets.service';
 | 
			
		||||
import { EChartsOption, registerMap } from 'echarts';
 | 
			
		||||
import { lerpColor } from 'src/app/shared/graphs.utils';
 | 
			
		||||
@ -17,11 +17,14 @@ import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
			
		||||
  styleUrls: ['./nodes-map.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodesMap implements OnInit {
 | 
			
		||||
export class NodesMap implements OnInit, OnChanges {
 | 
			
		||||
  @Input() widget: boolean = false;
 | 
			
		||||
  @Input() nodes: any[] | undefined = undefined;
 | 
			
		||||
  @Input() type: 'none' | 'isp' | 'country' = 'none';
 | 
			
		||||
  
 | 
			
		||||
  @Input() fitContainer = false;
 | 
			
		||||
  @Output() readyEvent = new EventEmitter();
 | 
			
		||||
  inputNodes$: BehaviorSubject<any>;
 | 
			
		||||
  nodes$: Observable<any>;
 | 
			
		||||
  observable$: Observable<any>;
 | 
			
		||||
 | 
			
		||||
  chartInstance = undefined;
 | 
			
		||||
@ -45,9 +48,17 @@ export class NodesMap implements OnInit {
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.seoService.setTitle($localize`Lightning nodes world map`);
 | 
			
		||||
 | 
			
		||||
    this.observable$ = zip(
 | 
			
		||||
    if (!this.inputNodes$) {
 | 
			
		||||
      this.inputNodes$ = new BehaviorSubject(this.nodes);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.nodes$ = this.inputNodes$.pipe(
 | 
			
		||||
      switchMap((nodes) =>  nodes ? [nodes] : this.apiService.getWorldNodes$())
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.observable$ = combineLatest(
 | 
			
		||||
      this.assetsService.getWorldMapJson$,
 | 
			
		||||
      this.nodes ? [this.nodes] : this.apiService.getWorldNodes$()
 | 
			
		||||
      this.nodes$
 | 
			
		||||
    ).pipe(tap((data) => {
 | 
			
		||||
      registerMap('world', data[0]);
 | 
			
		||||
 | 
			
		||||
@ -110,6 +121,16 @@ export class NodesMap implements OnInit {
 | 
			
		||||
    }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(changes): void {
 | 
			
		||||
    if (changes.nodes) {
 | 
			
		||||
      if (!this.inputNodes$) {
 | 
			
		||||
        this.inputNodes$ = new BehaviorSubject(changes.nodes.currentValue);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.inputNodes$.next(changes.nodes.currentValue);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom) {
 | 
			
		||||
    let title: object;
 | 
			
		||||
    if (nodes.length === 0) {
 | 
			
		||||
@ -224,4 +245,8 @@ export class NodesMap implements OnInit {
 | 
			
		||||
      this.chartInstance.resize();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChartFinished(e) {
 | 
			
		||||
    this.readyEvent.emit();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,63 @@
 | 
			
		||||
<div class="box preview-box" *ngIf="(nodes$ | async) as ispNodes">
 | 
			
		||||
  <app-preview-title>
 | 
			
		||||
    <span i18n="lightning.node">lightning ISP</span>
 | 
			
		||||
  </app-preview-title>
 | 
			
		||||
  <div class="row d-flex justify-content-between full-width-row">
 | 
			
		||||
    <div class="title-wrapper">
 | 
			
		||||
      <h1 class="title">{{ isp?.name }}</h1>
 | 
			
		||||
      <a class="subtitle" [routerLink]="['/lightning/nodes/isp/' | relativeUrl, isp?.id]">
 | 
			
		||||
        ASN {{ isp?.id }}
 | 
			
		||||
      </a>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="logo-wrapper">
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="row">
 | 
			
		||||
    <div class="col-md">
 | 
			
		||||
      <table class="table table-borderless table-striped table-fixed">
 | 
			
		||||
        <col span="1" width="250px">
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <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>
 | 
			
		||||
                <app-sats [satoshis]="ispNodes.sumLiquidity" digitsInfo="1.0-2"></app-sats>
 | 
			
		||||
              </ng-template>
 | 
			
		||||
            </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">
 | 
			
		||||
              <span class="">{{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }}</span>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td i18n="lightning.top-node">Top node</td>
 | 
			
		||||
            <td class="text-truncate">
 | 
			
		||||
              {{ ispNodes.nodes[0].alias }}
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-md map-col">
 | 
			
		||||
      <app-nodes-map [widget]="true" [nodes]="ispNodes.nodes" type="isp" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-map>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<ng-template [ngIf]="error">
 | 
			
		||||
  <div class="text-center">
 | 
			
		||||
    <span i18n="error.general-loading-data">Error loading data.</span>
 | 
			
		||||
  </div>
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,31 @@
 | 
			
		||||
.table {
 | 
			
		||||
  font-size: 32px;
 | 
			
		||||
  margin-top: 0px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.map-col {
 | 
			
		||||
  flex-grow: 0;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
  width: 470px;
 | 
			
		||||
  height: 360px;
 | 
			
		||||
  min-width: 470px;
 | 
			
		||||
  min-height: 360px;
 | 
			
		||||
  max-height: 360px;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  background: #181b2d;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.row {
 | 
			
		||||
  margin-right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.full-width-row {
 | 
			
		||||
  padding-left: 15px;
 | 
			
		||||
  flex-wrap: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::ng-deep .symbol {
 | 
			
		||||
  font-size: 24px;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,103 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { catchError, map, switchMap, Observable, share, of } from 'rxjs';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
 | 
			
		||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
			
		||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-per-isp-preview',
 | 
			
		||||
  templateUrl: './nodes-per-isp-preview.component.html',
 | 
			
		||||
  styleUrls: ['./nodes-per-isp-preview.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodesPerISPPreview implements OnInit {
 | 
			
		||||
  nodes$: Observable<any>;
 | 
			
		||||
  isp: {name: string, id: number};
 | 
			
		||||
  id: string;
 | 
			
		||||
  error: Error;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private openGraphService: OpenGraphService,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.nodes$ = this.route.paramMap
 | 
			
		||||
      .pipe(
 | 
			
		||||
        switchMap((params: ParamMap) => {
 | 
			
		||||
          this.id = params.get('isp');
 | 
			
		||||
          this.isp = null;
 | 
			
		||||
          this.openGraphService.waitFor('isp-map-' + this.id);
 | 
			
		||||
          this.openGraphService.waitFor('isp-data-' + this.id);
 | 
			
		||||
          return this.apiService.getNodeForISP$(params.get('isp'));
 | 
			
		||||
        }),
 | 
			
		||||
        map(response => {
 | 
			
		||||
          this.isp = {
 | 
			
		||||
            name: response.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}]`);
 | 
			
		||||
 | 
			
		||||
          for (const i in response.nodes) {
 | 
			
		||||
            response.nodes[i].geolocation = <GeolocationData>{
 | 
			
		||||
              country: response.nodes[i].country?.en,
 | 
			
		||||
              city: response.nodes[i].city?.en,
 | 
			
		||||
              subdivision: response.nodes[i].subdivision?.en,
 | 
			
		||||
              iso: response.nodes[i].iso_code,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          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);
 | 
			
		||||
 | 
			
		||||
          this.openGraphService.waitOver('isp-data-' + this.id);
 | 
			
		||||
 | 
			
		||||
          return {
 | 
			
		||||
            nodes: response.nodes,
 | 
			
		||||
            sumLiquidity: sumLiquidity,
 | 
			
		||||
            sumChannels: sumChannels,
 | 
			
		||||
            topCountry: topCountry,
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
        catchError(err => {
 | 
			
		||||
          this.error = err;
 | 
			
		||||
          this.openGraphService.fail('isp-map-' + this.id);
 | 
			
		||||
          this.openGraphService.fail('isp-data-' + this.id);
 | 
			
		||||
          return of({
 | 
			
		||||
            nodes: [],
 | 
			
		||||
            sumLiquidity: 0,
 | 
			
		||||
            sumChannels: 0,
 | 
			
		||||
            topCountry: {},
 | 
			
		||||
          });
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onMapReady() {
 | 
			
		||||
    this.openGraphService.waitOver('isp-map-' + this.id);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -46,6 +46,17 @@ const routes = {
 | 
			
		||||
          return `Lightning Channel: ${path[0]}`;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      nodes: {
 | 
			
		||||
        routes: {
 | 
			
		||||
          isp: {
 | 
			
		||||
            render: true,
 | 
			
		||||
            params: 1,
 | 
			
		||||
            getTitle(path) {
 | 
			
		||||
              return `Lightning ISP: ${path[0]}`;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mining: {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user