Merge pull request #2542 from mononaut/isp-preview
Add Lightning ISP preview
This commit is contained in:
		
						commit
						33334fd94c
					
				| @ -8,10 +8,12 @@ import { LightningApiService } from './lightning-api.service'; | |||||||
| import { NodePreviewComponent } from './node/node-preview.component'; | import { NodePreviewComponent } from './node/node-preview.component'; | ||||||
| import { LightningPreviewsRoutingModule } from './lightning-previews.routing.module'; | import { LightningPreviewsRoutingModule } from './lightning-previews.routing.module'; | ||||||
| import { ChannelPreviewComponent } from './channel/channel-preview.component'; | import { ChannelPreviewComponent } from './channel/channel-preview.component'; | ||||||
|  | import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component'; | ||||||
| @NgModule({ | @NgModule({ | ||||||
|   declarations: [ |   declarations: [ | ||||||
|     NodePreviewComponent, |     NodePreviewComponent, | ||||||
|     ChannelPreviewComponent, |     ChannelPreviewComponent, | ||||||
|  |     NodesPerISPPreview, | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     CommonModule, |     CommonModule, | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; | |||||||
| import { RouterModule, Routes } from '@angular/router'; | import { RouterModule, Routes } from '@angular/router'; | ||||||
| import { NodePreviewComponent } from './node/node-preview.component'; | import { NodePreviewComponent } from './node/node-preview.component'; | ||||||
| import { ChannelPreviewComponent } from './channel/channel-preview.component'; | import { ChannelPreviewComponent } from './channel/channel-preview.component'; | ||||||
|  | import { NodesPerISPPreview } from './nodes-per-isp/nodes-per-isp-preview.component'; | ||||||
| 
 | 
 | ||||||
| const routes: Routes = [ | const routes: Routes = [ | ||||||
|     { |     { | ||||||
| @ -12,6 +13,10 @@ const routes: Routes = [ | |||||||
|       path: 'channel/:short_id', |       path: 'channel/:short_id', | ||||||
|       component: ChannelPreviewComponent, |       component: ChannelPreviewComponent, | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |       path: 'nodes/isp/:isp', | ||||||
|  |       component: NodesPerISPPreview, | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       path: '**', |       path: '**', | ||||||
|       redirectTo: '' |       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 *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"> | ||||||
| @ -8,7 +8,7 @@ | |||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|   <div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" 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)" (chartFinished)="onChartFinished($event)"> | ||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -21,6 +21,17 @@ | |||||||
|   height: 240px; |   height: 240px; | ||||||
|   padding: 0px; |   padding: 0px; | ||||||
| } | } | ||||||
|  | .full-container.fit-container { | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
|  |   height: 100%; | ||||||
|  |   min-height: 100px; | ||||||
|  | 
 | ||||||
|  |   .chart { | ||||||
|  |     padding: 0; | ||||||
|  |     min-height: 100px; | ||||||
|  |   } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| .chart { | .chart { | ||||||
|   width: 100%; |   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 { 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, BehaviorSubject, switchMap, tap, combineLatest } from 'rxjs'; | ||||||
| import { AssetsService } from 'src/app/services/assets.service'; | import { AssetsService } from 'src/app/services/assets.service'; | ||||||
| import { EChartsOption, registerMap } from 'echarts'; | import { EChartsOption, registerMap } from 'echarts'; | ||||||
| import { lerpColor } from 'src/app/shared/graphs.utils'; | 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'], |   styleUrls: ['./nodes-map.component.scss'], | ||||||
|   changeDetection: ChangeDetectionStrategy.OnPush, |   changeDetection: ChangeDetectionStrategy.OnPush, | ||||||
| }) | }) | ||||||
| export class NodesMap implements OnInit { | export class NodesMap implements OnInit, OnChanges { | ||||||
|   @Input() widget: boolean = false; |   @Input() widget: boolean = false; | ||||||
|   @Input() nodes: any[] | undefined = undefined; |   @Input() nodes: any[] | undefined = undefined; | ||||||
|   @Input() type: 'none' | 'isp' | 'country' = 'none'; |   @Input() type: 'none' | 'isp' | 'country' = 'none'; | ||||||
|    |   @Input() fitContainer = false; | ||||||
|  |   @Output() readyEvent = new EventEmitter(); | ||||||
|  |   inputNodes$: BehaviorSubject<any>; | ||||||
|  |   nodes$: Observable<any>; | ||||||
|   observable$: Observable<any>; |   observable$: Observable<any>; | ||||||
| 
 | 
 | ||||||
|   chartInstance = undefined; |   chartInstance = undefined; | ||||||
| @ -45,9 +48,17 @@ export class NodesMap implements OnInit { | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.seoService.setTitle($localize`Lightning nodes world map`); |     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.assetsService.getWorldMapJson$, | ||||||
|       this.nodes ? [this.nodes] : this.apiService.getWorldNodes$() |       this.nodes$ | ||||||
|     ).pipe(tap((data) => { |     ).pipe(tap((data) => { | ||||||
|       registerMap('world', data[0]); |       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) { |   prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom) { | ||||||
|     let title: object; |     let title: object; | ||||||
|     if (nodes.length === 0) { |     if (nodes.length === 0) { | ||||||
| @ -224,4 +245,8 @@ export class NodesMap implements OnInit { | |||||||
|       this.chartInstance.resize(); |       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]}`; |           return `Lightning Channel: ${path[0]}`; | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|  |       nodes: { | ||||||
|  |         routes: { | ||||||
|  |           isp: { | ||||||
|  |             render: true, | ||||||
|  |             params: 1, | ||||||
|  |             getTitle(path) { | ||||||
|  |               return `Lightning ISP: ${path[0]}`; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mining: { |   mining: { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user