diff --git a/frontend/src/app/lightning/lightning-previews.module.ts b/frontend/src/app/lightning/lightning-previews.module.ts index 4d5d6cee9..0400acc55 100644 --- a/frontend/src/app/lightning/lightning-previews.module.ts +++ b/frontend/src/app/lightning/lightning-previews.module.ts @@ -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, diff --git a/frontend/src/app/lightning/lightning-previews.routing.module.ts b/frontend/src/app/lightning/lightning-previews.routing.module.ts index 69de2aadf..11250214d 100644 --- a/frontend/src/app/lightning/lightning-previews.routing.module.ts +++ b/frontend/src/app/lightning/lightning-previews.routing.module.ts @@ -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: '' diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.html b/frontend/src/app/lightning/nodes-map/nodes-map.component.html index d739dd2c9..f6a6f6009 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.html +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.html @@ -1,4 +1,4 @@ -
+
@@ -8,7 +8,7 @@
+ (chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.scss b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss index d7ad42b46..7a67e4eb4 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.scss +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.scss @@ -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%; diff --git a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts index b783e225a..8ec853aaa 100644 --- a/frontend/src/app/lightning/nodes-map/nodes-map.component.ts +++ b/frontend/src/app/lightning/nodes-map/nodes-map.component.ts @@ -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; + nodes$: Observable; observable$: Observable; 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(); + } } diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html new file mode 100644 index 000000000..4db69156f --- /dev/null +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.html @@ -0,0 +1,63 @@ +
+ + lightning ISP + +
+
+

{{ isp?.name }}

+ + ASN {{ isp?.id }} + +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
Nodes{{ ispNodes.nodes.length }}
Liquidity + + + + +
Channels{{ ispNodes.sumChannels }}
Top country + {{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }} +
Top node + {{ ispNodes.nodes[0].alias }} +
+
+
+ +
+
+
+ + +
+ Error loading data. +
+
diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.scss b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.scss new file mode 100644 index 000000000..2fe34ef5e --- /dev/null +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.scss @@ -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; +} \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts new file mode 100644 index 000000000..18e2f2d6c --- /dev/null +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp-preview.component.ts @@ -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; + 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 = { + 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); + } +} diff --git a/unfurler/src/routes.ts b/unfurler/src/routes.ts index c6e77e79a..4c25bf93b 100644 --- a/unfurler/src/routes.ts +++ b/unfurler/src/routes.ts @@ -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: {