Merge pull request #2283 from mononaut/lightning-unfurls
Lightning unfurls
This commit is contained in:
commit
097a763e6e
@ -366,6 +366,18 @@ let routes: Routes = [
|
|||||||
children: [],
|
children: [],
|
||||||
component: AddressPreviewComponent
|
component: AddressPreviewComponent
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'lightning',
|
||||||
|
loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'testnet/lightning',
|
||||||
|
loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'signet/lightning',
|
||||||
|
loadChildren: () => import('./lightning/lightning-previews.module').then(m => m.LightningPreviewsModule)
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -7,12 +7,12 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div [ngSwitch]="network.val">
|
<div [ngSwitch]="network.val">
|
||||||
<span *ngSwitchCase="'signet'" class="network signet"><img src="/resources/signet-logo.png" style="width: 45px;" class="signet mr-1" alt="logo"> Signet</span>
|
<span *ngSwitchCase="'signet'" class="network signet"><img src="/resources/signet-logo.png" style="width: 45px;" class="signet mr-1" alt="logo"> Signet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||||
<span *ngSwitchCase="'testnet'" class="network testnet"><img src="/resources/testnet-logo.png" style="width: 45px;" class="mr-1" alt="testnet logo"> Testnet</span>
|
<span *ngSwitchCase="'testnet'" class="network testnet"><img src="/resources/testnet-logo.png" style="width: 45px;" class="mr-1" alt="testnet logo"> Testnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||||
<span *ngSwitchCase="'bisq'" class="network bisq"><img src="/resources/bisq-logo.png" style="width: 45px;" class="mr-1" alt="bisq logo"> Bisq</span>
|
<span *ngSwitchCase="'bisq'" class="network bisq"><img src="/resources/bisq-logo.png" style="width: 45px;" class="mr-1" alt="bisq logo"> Bisq</span>
|
||||||
<span *ngSwitchCase="'liquid'" class="network liquid"><img src="/resources/liquid-logo.png" style="width: 45px;" class="mr-1" alt="liquid mainnet logo"> Liquid</span>
|
<span *ngSwitchCase="'liquid'" class="network liquid"><img src="/resources/liquid-logo.png" style="width: 45px;" class="mr-1" alt="liquid mainnet logo"> Liquid</span>
|
||||||
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><img src="/resources/liquidtestnet-logo.png" style="width: 45px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</span>
|
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><img src="/resources/liquidtestnet-logo.png" style="width: 45px;" class="mr-1" alt="liquid testnet logo"> Liquid Testnet</span>
|
||||||
<span *ngSwitchDefault class="network mainnet"><img src="/resources/bitcoin-logo.png" style="width: 45px;" class="mainnet mr-1" alt="bitcoin logo"> Mainnet</span>
|
<span *ngSwitchDefault class="network mainnet"><img src="/resources/bitcoin-logo.png" style="width: 45px;" class="mainnet mr-1" alt="bitcoin logo"> Mainnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
@ -10,6 +10,7 @@ import { LanguageService } from 'src/app/services/language.service';
|
|||||||
})
|
})
|
||||||
export class MasterPagePreviewComponent implements OnInit {
|
export class MasterPagePreviewComponent implements OnInit {
|
||||||
network$: Observable<string>;
|
network$: Observable<string>;
|
||||||
|
lightning$: Observable<boolean>;
|
||||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||||
urlLanguage: string;
|
urlLanguage: string;
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ export class MasterPagePreviewComponent implements OnInit {
|
|||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
this.network$ = merge(of(''), this.stateService.networkChanged$);
|
||||||
|
this.lightning$ = this.stateService.lightningChanged$;
|
||||||
this.urlLanguage = this.languageService.getLanguageForUrl();
|
this.urlLanguage = this.languageService.getLanguageForUrl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
<div class="box preview-box" *ngIf="(channel$ | async) as channel">
|
||||||
|
<div class="row d-flex justify-content-between full-width-row">
|
||||||
|
<h1 class="title">
|
||||||
|
<span i18n="lightning.channel">Channel</span>:
|
||||||
|
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]"> {{ channel.short_id }}</a>
|
||||||
|
</h1>
|
||||||
|
<div class="badges mb-2">
|
||||||
|
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
|
||||||
|
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
|
||||||
|
<span class="badge rounded-pill badge-danger" *ngIf="channel.status === 2">Closed</span>
|
||||||
|
|
||||||
|
<app-closing-type [type]="channel.closing_reason" *ngIf="channel.status === 2"></app-closing-type>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row d-flex justify-content-between full-width-row nodes">
|
||||||
|
<span class="node left">
|
||||||
|
{{ channel.node_left.alias || '?' }}
|
||||||
|
</span>
|
||||||
|
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true" title="channel between"></fa-icon>
|
||||||
|
<span class="node right">
|
||||||
|
{{ channel.node_right.alias || '?' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="channel.created">Created</td>
|
||||||
|
<td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="channel.capacity">Capacity</td>
|
||||||
|
<td><app-amount [satoshis]="channel.capacity" [noFiat]="true"></app-amount></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="channel.fee-rate">Fee rate</td>
|
||||||
|
<td>
|
||||||
|
<div class="dual-cell">
|
||||||
|
<span>{{ channel.node_left.fee_rate }} <span class="symbol">ppm</span></span>
|
||||||
|
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true"></fa-icon>
|
||||||
|
<span>{{ channel.node_right.fee_rate }} <span class="symbol">ppm</span></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="channel.base-fee">Base fee</td>
|
||||||
|
<td>
|
||||||
|
<div class="dual-cell">
|
||||||
|
<app-sats [satoshis]="channel.node_left.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats>
|
||||||
|
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true"></fa-icon>
|
||||||
|
<app-sats [satoshis]="channel.node_right.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md map-col">
|
||||||
|
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-template [ngIf]="error">
|
||||||
|
<div class="text-center">
|
||||||
|
<span i18n="error.general-loading-data">Error loading data.</span>
|
||||||
|
<br><br>
|
||||||
|
<i>{{ error.status }}: {{ error.error }}</i>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
@ -0,0 +1,76 @@
|
|||||||
|
.title {
|
||||||
|
font-size: 52px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-top: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badges {
|
||||||
|
font-size: 28px;
|
||||||
|
|
||||||
|
::ng-deep .badge {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width-row {
|
||||||
|
padding-left: 15px;
|
||||||
|
padding-right: 15px;
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background: #181b2d;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes {
|
||||||
|
font-size: 36px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.between-arrow {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-col {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 470px;
|
||||||
|
min-width: 470px;
|
||||||
|
padding: 0;
|
||||||
|
background: #181b2d;
|
||||||
|
max-height: 470px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .symbol {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dual-cell {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 1.5em;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { catchError, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
|
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||||
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-channel-preview',
|
||||||
|
templateUrl: './channel-preview.component.html',
|
||||||
|
styleUrls: ['./channel-preview.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class ChannelPreviewComponent implements OnInit {
|
||||||
|
channel$: Observable<any>;
|
||||||
|
error: any = null;
|
||||||
|
channelGeo: number[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private lightningApiService: LightningApiService,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private seoService: SeoService,
|
||||||
|
private openGraphService: OpenGraphService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.channel$ = this.activatedRoute.paramMap
|
||||||
|
.pipe(
|
||||||
|
switchMap((params: ParamMap) => {
|
||||||
|
this.openGraphService.waitFor('channel-map');
|
||||||
|
this.openGraphService.waitFor('channel-data');
|
||||||
|
this.error = null;
|
||||||
|
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
|
||||||
|
return this.lightningApiService.getChannel$(params.get('short_id'))
|
||||||
|
.pipe(
|
||||||
|
tap((data) => {
|
||||||
|
if (!data.node_left.longitude || !data.node_left.latitude ||
|
||||||
|
!data.node_right.longitude || !data.node_right.latitude) {
|
||||||
|
this.channelGeo = [];
|
||||||
|
} else {
|
||||||
|
this.channelGeo = [
|
||||||
|
data.node_left.public_key,
|
||||||
|
data.node_left.alias,
|
||||||
|
data.node_left.longitude, data.node_left.latitude,
|
||||||
|
data.node_right.public_key,
|
||||||
|
data.node_right.alias,
|
||||||
|
data.node_right.longitude, data.node_right.latitude,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
this.openGraphService.waitOver('channel-data');
|
||||||
|
}),
|
||||||
|
catchError((err) => {
|
||||||
|
this.error = err;
|
||||||
|
this.openGraphService.fail('channel-map');
|
||||||
|
this.openGraphService.fail('channel-data');
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMapReady() {
|
||||||
|
this.openGraphService.waitOver('channel-map');
|
||||||
|
}
|
||||||
|
}
|
28
frontend/src/app/lightning/lightning-previews.module.ts
Normal file
28
frontend/src/app/lightning/lightning-previews.module.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GraphsModule } from '../graphs/graphs.module';
|
||||||
|
import { LightningModule } from './lightning.module';
|
||||||
|
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';
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
NodePreviewComponent,
|
||||||
|
ChannelPreviewComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
RouterModule,
|
||||||
|
GraphsModule,
|
||||||
|
LightningPreviewsRoutingModule,
|
||||||
|
LightningModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
LightningApiService,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LightningPreviewsModule { }
|
@ -0,0 +1,25 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'node/:public_key',
|
||||||
|
component: NodePreviewComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'channel/:short_id',
|
||||||
|
component: ChannelPreviewComponent,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '**',
|
||||||
|
redirectTo: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class LightningPreviewsRoutingModule { }
|
@ -53,6 +53,27 @@ import { NodesChannelsMap } from '../lightning/nodes-channels-map/nodes-channels
|
|||||||
LightningRoutingModule,
|
LightningRoutingModule,
|
||||||
GraphsModule,
|
GraphsModule,
|
||||||
],
|
],
|
||||||
|
exports: [
|
||||||
|
LightningDashboardComponent,
|
||||||
|
NodesListComponent,
|
||||||
|
NodeStatisticsComponent,
|
||||||
|
NodeStatisticsChartComponent,
|
||||||
|
NodeComponent,
|
||||||
|
ChannelsListComponent,
|
||||||
|
ChannelComponent,
|
||||||
|
LightningWrapperComponent,
|
||||||
|
ChannelBoxComponent,
|
||||||
|
ClosingTypeComponent,
|
||||||
|
LightningStatisticsChartComponent,
|
||||||
|
NodesNetworksChartComponent,
|
||||||
|
ChannelsStatisticsComponent,
|
||||||
|
NodesPerISPChartComponent,
|
||||||
|
NodesPerCountry,
|
||||||
|
NodesPerISP,
|
||||||
|
NodesPerCountryChartComponent,
|
||||||
|
NodesMap,
|
||||||
|
NodesChannelsMap,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
LightningApiService,
|
LightningApiService,
|
||||||
]
|
]
|
||||||
|
64
frontend/src/app/lightning/node/node-preview.component.html
Normal file
64
frontend/src/app/lightning/node/node-preview.component.html
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<div class="box preview-box" *ngIf="(node$ | async) as node">
|
||||||
|
<div class="row d-flex justify-content-between full-width-row">
|
||||||
|
<h1 class="title">
|
||||||
|
<span i18n="lightning.node">Node</span>:
|
||||||
|
<a [routerLink]="['/lightning/node' | relativeUrl, node.id]"> {{ node.alias }}</a>
|
||||||
|
</h1>
|
||||||
|
<div class="badges mb-2">
|
||||||
|
<span class="badge rounded-pill badge-success" *ngFor="let socketType of socketTypes">{{ socketType }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md">
|
||||||
|
<table class="table table-borderless table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.active-capacity">Active capacity</td>
|
||||||
|
<td>
|
||||||
|
<app-amount [satoshis]="node.capacity" [noFiat]="true"></app-amount>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.active-channels">Active channels</td>
|
||||||
|
<td>
|
||||||
|
{{ node.active_channel_count }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td i18n="lightning.active-channels-avg">Average size</td>
|
||||||
|
<td>
|
||||||
|
<app-amount [satoshis]="node.avgCapacity" [noFiat]="true"></app-amount>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="node.city">
|
||||||
|
<td i18n="location">Location</td>
|
||||||
|
<td>
|
||||||
|
<span>{{ node.city.en }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="node.country">
|
||||||
|
<td i18n="country">Country</td>
|
||||||
|
<td>
|
||||||
|
{{ node.country.en }} {{ node.flag }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr *ngIf="!node.city && !node.country">
|
||||||
|
<td i18n="location">Location</td>
|
||||||
|
<td>
|
||||||
|
<span>unknown</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md map-col">
|
||||||
|
<app-nodes-channels-map *ngIf="!error" [style]="'nodepage'" [publicKey]="node.public_key" [fitContainer]="true" (readyEvent)="onMapReady()"></app-nodes-channels-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>
|
43
frontend/src/app/lightning/node/node-preview.component.scss
Normal file
43
frontend/src/app/lightning/node/node-preview.component.scss
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.title {
|
||||||
|
font-size: 52px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-top: 48px;
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badges {
|
||||||
|
font-size: 28px;
|
||||||
|
|
||||||
|
::ng-deep .badge {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-col {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 470px;
|
||||||
|
height: 390px;
|
||||||
|
min-width: 470px;
|
||||||
|
min-height: 390px;
|
||||||
|
max-height: 390px;
|
||||||
|
padding: 0;
|
||||||
|
background: #181b2d;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width-row {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .symbol {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
105
frontend/src/app/lightning/node/node-preview.component.ts
Normal file
105
frontend/src/app/lightning/node/node-preview.component.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { catchError, map, switchMap } from 'rxjs/operators';
|
||||||
|
import { SeoService } from 'src/app/services/seo.service';
|
||||||
|
import { OpenGraphService } from 'src/app/services/opengraph.service';
|
||||||
|
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
|
||||||
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
|
import { isMobile } from '../../shared/common.utils';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-node-preview',
|
||||||
|
templateUrl: './node-preview.component.html',
|
||||||
|
styleUrls: ['./node-preview.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class NodePreviewComponent implements OnInit {
|
||||||
|
node$: Observable<any>;
|
||||||
|
statistics$: Observable<any>;
|
||||||
|
publicKey$: Observable<string>;
|
||||||
|
selectedSocketIndex = 0;
|
||||||
|
qrCodeVisible = false;
|
||||||
|
channelsListStatus: string;
|
||||||
|
error: Error;
|
||||||
|
publicKey: string;
|
||||||
|
socketTypes: string[];
|
||||||
|
|
||||||
|
publicKeySize = 99;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private lightningApiService: LightningApiService,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private seoService: SeoService,
|
||||||
|
private openGraphService: OpenGraphService,
|
||||||
|
) {
|
||||||
|
if (isMobile()) {
|
||||||
|
this.publicKeySize = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.node$ = this.activatedRoute.paramMap
|
||||||
|
.pipe(
|
||||||
|
switchMap((params: ParamMap) => {
|
||||||
|
this.openGraphService.waitFor('node-map');
|
||||||
|
this.openGraphService.waitFor('node-data');
|
||||||
|
this.publicKey = params.get('public_key');
|
||||||
|
return this.lightningApiService.getNode$(params.get('public_key'));
|
||||||
|
}),
|
||||||
|
map((node) => {
|
||||||
|
this.seoService.setTitle(`Node: ${node.alias}`);
|
||||||
|
|
||||||
|
const socketsObject = [];
|
||||||
|
const socketTypesMap = {};
|
||||||
|
for (const socket of node.sockets.split(',')) {
|
||||||
|
if (socket === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let label = '';
|
||||||
|
if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) {
|
||||||
|
label = 'IPv4';
|
||||||
|
} else if (socket.indexOf('[') > -1) {
|
||||||
|
label = 'IPv6';
|
||||||
|
} else if (socket.indexOf('onion') > -1) {
|
||||||
|
label = 'Tor';
|
||||||
|
}
|
||||||
|
node.flag = getFlagEmoji(node.iso_code);
|
||||||
|
socketsObject.push({
|
||||||
|
label: label,
|
||||||
|
socket: node.public_key + '@' + socket,
|
||||||
|
});
|
||||||
|
socketTypesMap[label] = true
|
||||||
|
}
|
||||||
|
node.socketsObject = socketsObject;
|
||||||
|
this.socketTypes = Object.keys(socketTypesMap);
|
||||||
|
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
|
||||||
|
|
||||||
|
this.openGraphService.waitOver('node-data');
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
this.error = err;
|
||||||
|
this.openGraphService.fail('node-map');
|
||||||
|
this.openGraphService.fail('node-data');
|
||||||
|
return [{
|
||||||
|
alias: this.publicKey,
|
||||||
|
public_key: this.publicKey,
|
||||||
|
}];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
changeSocket(index: number) {
|
||||||
|
this.selectedSocketIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
onChannelsListStatusChanged(e) {
|
||||||
|
this.channelsListStatus = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMapReady() {
|
||||||
|
this.openGraphService.waitOver('node-map');
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
<div [class]="'full-container ' + style">
|
<div [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||||
|
|
||||||
<div *ngIf="style === 'graph'" class="card-header">
|
<div *ngIf="style === 'graph'" 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" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||||
(chartInit)="onChartInit($event)">
|
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,6 +29,18 @@
|
|||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full-container.fit-container {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
padding: 0;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.widget {
|
.widget {
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, HostListener, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, HostListener, Input, Output, EventEmitter, NgZone, OnDestroy, OnInit, ViewChild } 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, switchMap, tap, zip } from 'rxjs';
|
import { Observable, switchMap, tap, zip } from 'rxjs';
|
||||||
@ -20,9 +20,11 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
|
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
|
||||||
@Input() publicKey: string | undefined;
|
@Input() publicKey: string | undefined;
|
||||||
@Input() channel: any[] = [];
|
@Input() channel: any[] = [];
|
||||||
|
@Input() fitContainer = false;
|
||||||
|
@Output() readyEvent = new EventEmitter();
|
||||||
|
|
||||||
observable$: Observable<any>;
|
observable$: Observable<any>;
|
||||||
|
|
||||||
center: number[] | undefined;
|
center: number[] | undefined;
|
||||||
zoom: number | undefined;
|
zoom: number | undefined;
|
||||||
channelWidth = 0.6;
|
channelWidth = 0.6;
|
||||||
@ -313,4 +315,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
|
|||||||
this.chartInstance.setOption(chartOptions);
|
this.chartInstance.setOption(chartOptions);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChartFinished(e) {
|
||||||
|
this.readyEvent.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,11 +71,13 @@ const defaultEnv: Env = {
|
|||||||
export class StateService {
|
export class StateService {
|
||||||
isBrowser: boolean = isPlatformBrowser(this.platformId);
|
isBrowser: boolean = isPlatformBrowser(this.platformId);
|
||||||
network = '';
|
network = '';
|
||||||
|
lightning = false;
|
||||||
blockVSize: number;
|
blockVSize: number;
|
||||||
env: Env;
|
env: Env;
|
||||||
latestBlockHeight = -1;
|
latestBlockHeight = -1;
|
||||||
|
|
||||||
networkChanged$ = new ReplaySubject<string>(1);
|
networkChanged$ = new ReplaySubject<string>(1);
|
||||||
|
lightningChanged$ = new ReplaySubject<boolean>(1);
|
||||||
blocks$: ReplaySubject<[BlockExtended, boolean]>;
|
blocks$: ReplaySubject<[BlockExtended, boolean]>;
|
||||||
transactions$ = new ReplaySubject<TransactionStripped>(6);
|
transactions$ = new ReplaySubject<TransactionStripped>(6);
|
||||||
conversions$ = new ReplaySubject<any>(1);
|
conversions$ = new ReplaySubject<any>(1);
|
||||||
@ -122,15 +124,18 @@ export class StateService {
|
|||||||
|
|
||||||
if (this.isBrowser) {
|
if (this.isBrowser) {
|
||||||
this.setNetworkBasedonUrl(window.location.pathname);
|
this.setNetworkBasedonUrl(window.location.pathname);
|
||||||
|
this.setLightningBasedonUrl(window.location.pathname);
|
||||||
this.isTabHidden$ = fromEvent(document, 'visibilitychange').pipe(map(() => this.isHidden()), shareReplay());
|
this.isTabHidden$ = fromEvent(document, 'visibilitychange').pipe(map(() => this.isHidden()), shareReplay());
|
||||||
} else {
|
} else {
|
||||||
this.setNetworkBasedonUrl('/');
|
this.setNetworkBasedonUrl('/');
|
||||||
|
this.setLightningBasedonUrl('/');
|
||||||
this.isTabHidden$ = new BehaviorSubject(false);
|
this.isTabHidden$ = new BehaviorSubject(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.router.events.subscribe((event) => {
|
this.router.events.subscribe((event) => {
|
||||||
if (event instanceof NavigationStart) {
|
if (event instanceof NavigationStart) {
|
||||||
this.setNetworkBasedonUrl(event.url);
|
this.setNetworkBasedonUrl(event.url);
|
||||||
|
this.setLightningBasedonUrl(event.url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -198,6 +203,15 @@ export class StateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLightningBasedonUrl(url: string) {
|
||||||
|
if (this.env.BASE_MODULE !== 'mempool') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const networkMatches = url.match(/\/lightning\//);
|
||||||
|
this.lightning = !!networkMatches;
|
||||||
|
this.lightningChanged$.next(this.lightning);
|
||||||
|
}
|
||||||
|
|
||||||
getHiddenProp(){
|
getHiddenProp(){
|
||||||
const prefixes = ['webkit', 'moz', 'ms', 'o'];
|
const prefixes = ['webkit', 'moz', 'ms', 'o'];
|
||||||
if ('hidden' in document) { return 'hidden'; }
|
if ('hidden' in document) { return 'hidden'; }
|
||||||
|
@ -4,7 +4,7 @@ import { NgbCollapse, NgbCollapseModule, NgbRadioGroup, NgbTypeaheadModule } fro
|
|||||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
|
||||||
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
|
||||||
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
|
||||||
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode } from '@fortawesome/free-solid-svg-icons';
|
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
import { MasterPageComponent } from '../components/master-page/master-page.component';
|
import { MasterPageComponent } from '../components/master-page/master-page.component';
|
||||||
import { MasterPagePreviewComponent } from '../components/master-page-preview/master-page-preview.component';
|
import { MasterPagePreviewComponent } from '../components/master-page-preview/master-page-preview.component';
|
||||||
@ -297,5 +297,6 @@ export class SharedModule {
|
|||||||
library.addIcons(faListUl);
|
library.addIcons(faListUl);
|
||||||
library.addIcons(faDownload);
|
library.addIcons(faDownload);
|
||||||
library.addIcons(faQrcode);
|
library.addIcons(faQrcode);
|
||||||
|
library.addIcons(faArrowRightArrowLeft);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,12 +150,27 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handle supported preview routes
|
// handle supported preview routes
|
||||||
if (parts[0] === 'block') {
|
switch (parts[0]) {
|
||||||
ogTitle = `Block: ${parts[1]}`;
|
case 'block':
|
||||||
} else if (parts[0] === 'address') {
|
ogTitle = `Block: ${parts[1]}`;
|
||||||
ogTitle = `Address: ${parts[1]}`;
|
break;
|
||||||
} else {
|
case 'address':
|
||||||
previewSupported = false;
|
ogTitle = `Address: ${parts[1]}`;
|
||||||
|
break;
|
||||||
|
case 'lightning':
|
||||||
|
switch (parts[1]) {
|
||||||
|
case 'node':
|
||||||
|
ogTitle = `Lightning Node: ${parts[2]}`;
|
||||||
|
break;
|
||||||
|
case 'channel':
|
||||||
|
ogTitle = `Lightning Channel: ${parts[2]}`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
previewSupported = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
previewSupported = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previewSupported) {
|
if (previewSupported) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user