Channel pagination
This commit is contained in:
parent
11a7babbc4
commit
473cb55dc4
@ -34,7 +34,7 @@ export class AddressLabelsComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleChannel() {
|
handleChannel() {
|
||||||
this.label = `Channel open: ${this.channel.alias_left} <> ${this.channel.alias_right}`;
|
this.label = `Channel open: ${this.channel.node_left.alias} <> ${this.channel.node_right.alias}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleVin() {
|
handleVin() {
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ChannelBoxComponent } from './channel-box.component';
|
||||||
|
|
||||||
|
describe('ChannelBoxComponent', () => {
|
||||||
|
let component: ChannelBoxComponent;
|
||||||
|
let fixture: ComponentFixture<ChannelBoxComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [ ChannelBoxComponent ]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ChannelBoxComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -1,44 +1,39 @@
|
|||||||
<div>
|
<div *ngIf="channels$ | async as response; else skeleton">
|
||||||
|
<h2 class="float-left">Channels ({{ response.totalItems }})</h2>
|
||||||
|
|
||||||
|
<form [formGroup]="channelStatusForm" class="formRadioGroup float-right">
|
||||||
|
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="status">
|
||||||
|
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||||
|
<input ngbButton type="radio" [value]="'open'" fragment="open"> Open
|
||||||
|
</label>
|
||||||
|
<label ngbButtonLabel class="btn-primary btn-sm">
|
||||||
|
<input ngbButton type="radio" [value]="'closed'" fragment="closed"> Closed
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<table class="table table-borderless">
|
<table class="table table-borderless">
|
||||||
<thead>
|
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
||||||
<th class="alias text-left" i18n="nodes.alias">Node Alias</th>
|
<tbody>
|
||||||
<th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction"> </th>
|
<tr *ngFor="let channel of response.channels; let i = index;">
|
||||||
<th class="alias text-left d-none d-md-table-cell" i18n="nodes.alias">Status</th>
|
|
||||||
<th class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
|
|
||||||
<th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
|
|
||||||
<th class="capacity text-left" i18n="channels.id">Channel ID</th>
|
|
||||||
</thead>
|
|
||||||
<tbody *ngIf="channels$ | async as channels; else skeleton">
|
|
||||||
<tr *ngFor="let channel of channels; let i = index;">
|
|
||||||
<ng-container *ngTemplateOutlet="tableTemplate; context: { $implicit: channel, node: channel.node_left.public_key === publicKey ? channel.node_right : channel.node_left }"></ng-container>
|
<ng-container *ngTemplateOutlet="tableTemplate; context: { $implicit: channel, node: channel.node_left.public_key === publicKey ? channel.node_right : channel.node_left }"></ng-container>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
<ng-template #skeleton>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
|
||||||
<td class="alias text-left" style="width: 370px;">
|
|
||||||
<span class="skeleton-loader"></span>
|
|
||||||
</td>
|
|
||||||
<td class="alias text-left">
|
|
||||||
<span class="skeleton-loader"></span>
|
|
||||||
</td>
|
|
||||||
<td class="capacity text-left d-none d-md-table-cell">
|
|
||||||
<span class="skeleton-loader"></span>
|
|
||||||
</td>
|
|
||||||
<td class="channels text-left d-none d-md-table-cell">
|
|
||||||
<span class="skeleton-loader"></span>
|
|
||||||
</td>
|
|
||||||
<td class="channels text-right d-none d-md-table-cell">
|
|
||||||
<span class="skeleton-loader"></span>
|
|
||||||
</td>
|
|
||||||
<td class="channels text-left">
|
|
||||||
<span class="skeleton-loader"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</ng-template>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<ngb-pagination class="pagination-container float-right" [size]="paginationSize" [collectionSize]="response.totalItems" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #tableHeader>
|
||||||
|
<thead>
|
||||||
|
<th class="alias text-left" i18n="nodes.alias">Node Alias</th>
|
||||||
|
<th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction"> </th>
|
||||||
|
<th class="alias text-left d-none d-md-table-cell" i18n="nodes.alias">Status</th>
|
||||||
|
<th class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
|
||||||
|
<th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
|
||||||
|
<th class="capacity text-right" i18n="channels.id">Channel ID</th>
|
||||||
|
</thead>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #tableTemplate let-channel let-node="node">
|
<ng-template #tableTemplate let-channel let-node="node">
|
||||||
<td class="alias text-left">
|
<td class="alias text-left">
|
||||||
@ -50,7 +45,7 @@
|
|||||||
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
|
<app-clipboard [text]="node.public_key" size="small"></app-clipboard>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="alias text-left">
|
<td class="alias text-left d-none d-md-table-cell">
|
||||||
<div class="second-line">{{ node.channels }} channels</div>
|
<div class="second-line">{{ node.channels }} channels</div>
|
||||||
<div class="second-line"><app-amount [satoshis]="node.capacity" digitsInfo="1.2-2"></app-amount></div>
|
<div class="second-line"><app-amount [satoshis]="node.capacity" digitsInfo="1.2-2"></app-amount></div>
|
||||||
</td>
|
</td>
|
||||||
@ -65,7 +60,37 @@
|
|||||||
<td class="capacity text-right d-none d-md-table-cell">
|
<td class="capacity text-right d-none d-md-table-cell">
|
||||||
<app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount>
|
<app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount>
|
||||||
</td>
|
</td>
|
||||||
<td class="capacity text-left">
|
<td class="capacity text-right">
|
||||||
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a>
|
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a>
|
||||||
</td>
|
</td>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #skeleton>
|
||||||
|
<h2 class="float-left">Channels</h2>
|
||||||
|
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<ng-container *ngTemplateOutlet="tableHeader"></ng-container>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
|
||||||
|
<td class="alias text-left" style="width: 370px;">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="alias text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="capacity text-left d-none d-md-table-cell">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="channels text-left d-none d-md-table-cell">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="channels text-right d-none d-md-table-cell">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
<td class="channels text-left">
|
||||||
|
<span class="skeleton-loader"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</ng-template>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||||
|
import { BehaviorSubject, combineLatest, merge, Observable, of } from 'rxjs';
|
||||||
|
import { map, startWith, switchMap } from 'rxjs/operators';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -8,16 +10,53 @@ import { LightningApiService } from '../lightning-api.service';
|
|||||||
styleUrls: ['./channels-list.component.scss'],
|
styleUrls: ['./channels-list.component.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ChannelsListComponent implements OnChanges {
|
export class ChannelsListComponent implements OnInit, OnChanges {
|
||||||
@Input() publicKey: string;
|
@Input() publicKey: string;
|
||||||
channels$: Observable<any[]>;
|
channels$: Observable<any>;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
paginationSize: 'sm' | 'lg' = 'md';
|
||||||
|
paginationMaxSize = 10;
|
||||||
|
itemsPerPage = 25;
|
||||||
|
page = 1;
|
||||||
|
channelsPage$ = new BehaviorSubject<number>(1);
|
||||||
|
channelStatusForm: FormGroup;
|
||||||
|
defaultStatus = 'open';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private lightningApiService: LightningApiService,
|
private lightningApiService: LightningApiService,
|
||||||
) { }
|
private formBuilder: FormBuilder,
|
||||||
|
) {
|
||||||
|
this.channelStatusForm = this.formBuilder.group({
|
||||||
|
status: [this.defaultStatus],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (document.body.clientWidth < 670) {
|
||||||
|
this.paginationSize = 'sm';
|
||||||
|
this.paginationMaxSize = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
ngOnChanges(): void {
|
||||||
this.channels$ = this.lightningApiService.getChannelsByNodeId$(this.publicKey);
|
this.channels$ = combineLatest([
|
||||||
|
this.channelsPage$,
|
||||||
|
this.channelStatusForm.get('status').valueChanges.pipe(startWith(this.defaultStatus))
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
switchMap(([page, status]) =>this.lightningApiService.getChannelsByNodeId$(this.publicKey, (page -1) * this.itemsPerPage, status)),
|
||||||
|
map((response) => {
|
||||||
|
return {
|
||||||
|
channels: response.body,
|
||||||
|
totalItems: parseInt(response.headers.get('x-total-count'), 10)
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pageChange(page: number) {
|
||||||
|
this.channelsPage$.next(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,14 @@ export class LightningApiService {
|
|||||||
return this.httpClient.get<any>(API_BASE_URL + '/channels/' + shortId);
|
return this.httpClient.get<any>(API_BASE_URL + '/channels/' + shortId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelsByNodeId$(publicKey: string): Observable<any> {
|
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
.set('public_key', publicKey)
|
.set('public_key', publicKey)
|
||||||
|
.set('index', index)
|
||||||
|
.set('status', status)
|
||||||
;
|
;
|
||||||
|
|
||||||
return this.httpClient.get<any>(API_BASE_URL + '/channels', { params });
|
return this.httpClient.get<any>(API_BASE_URL + '/channels', { params, observe: 'response' });
|
||||||
}
|
}
|
||||||
|
|
||||||
getLatestStatistics$(): Observable<any> {
|
getLatestStatistics$(): Observable<any> {
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<h2>Channels</h2>
|
|
||||||
|
|
||||||
<app-channels-list [publicKey]="node.public_key"></app-channels-list>
|
<app-channels-list [publicKey]="node.public_key"></app-channels-list>
|
||||||
|
|
||||||
|
@ -73,10 +73,16 @@ class ChannelsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getChannelsForNode(public_key: string): Promise<any> {
|
public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*, ns1.channels AS channels_left, ns1.capacity AS capacity_left, ns2.channels AS channels_right, ns2.capacity AS capacity_right FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key LEFT JOIN node_stats AS ns1 ON ns1.public_key = channels.node1_public_key LEFT JOIN node_stats AS ns2 ON ns2.public_key = channels.node2_public_key WHERE (ns1.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node1_public_key) AND ns2.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node2_public_key)) AND (node1_public_key = ? OR node2_public_key = ?) ORDER BY channels.capacity DESC`;
|
// Default active and inactive channels
|
||||||
const [rows]: any = await DB.query(query, [public_key, public_key]);
|
let statusQuery = '< 2';
|
||||||
|
// Closed channels only
|
||||||
|
if (status === 'closed') {
|
||||||
|
statusQuery = '= 2';
|
||||||
|
}
|
||||||
|
const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*, ns1.channels AS channels_left, ns1.capacity AS capacity_left, ns2.channels AS channels_right, ns2.capacity AS capacity_right FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key LEFT JOIN node_stats AS ns1 ON ns1.public_key = channels.node1_public_key LEFT JOIN node_stats AS ns2 ON ns2.public_key = channels.node2_public_key WHERE (ns1.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node1_public_key) AND ns2.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node2_public_key)) AND (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery} ORDER BY channels.capacity DESC LIMIT ?, ?`;
|
||||||
|
const [rows]: any = await DB.query(query, [public_key, public_key, index, length]);
|
||||||
const channels = rows.map((row) => this.convertChannel(row));
|
const channels = rows.map((row) => this.convertChannel(row));
|
||||||
return channels;
|
return channels;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -85,13 +91,30 @@ class ChannelsApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async $getChannelsCountForNode(public_key: string, status: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
// Default active and inactive channels
|
||||||
|
let statusQuery = '< 2';
|
||||||
|
// Closed channels only
|
||||||
|
if (status === 'closed') {
|
||||||
|
statusQuery = '= 2';
|
||||||
|
}
|
||||||
|
const query = `SELECT COUNT(*) AS count FROM channels WHERE (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery}`;
|
||||||
|
const [rows]: any = await DB.query(query, [public_key, public_key]);
|
||||||
|
return rows[0]['count'];
|
||||||
|
} catch (e) {
|
||||||
|
logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e));
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private convertChannel(channel: any): any {
|
private convertChannel(channel: any): any {
|
||||||
return {
|
return {
|
||||||
'id': channel.id,
|
'id': channel.id,
|
||||||
'short_id': channel.short_id,
|
'short_id': channel.short_id,
|
||||||
'capacity': channel.capacity,
|
'capacity': channel.capacity,
|
||||||
'transaction_id': channel.transaction_id,
|
'transaction_id': channel.transaction_id,
|
||||||
'transaction_vout': channel.void,
|
'transaction_vout': channel.transaction_vout,
|
||||||
'updated_at': channel.updated_at,
|
'updated_at': channel.updated_at,
|
||||||
'created': channel.created,
|
'created': channel.created,
|
||||||
'status': channel.status,
|
'status': channel.status,
|
||||||
|
@ -10,7 +10,7 @@ class ChannelsRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannels)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannelsForNode)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,13 +36,18 @@ class ChannelsRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $getChannels(req: Request, res: Response) {
|
private async $getChannelsForNode(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
if (typeof req.query.public_key !== 'string') {
|
if (typeof req.query.public_key !== 'string') {
|
||||||
res.status(501).send('Missing parameter: public_key');
|
res.status(501).send('Missing parameter: public_key');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const channels = await channelsApi.$getChannelsForNode(req.query.public_key);
|
const index = parseInt(typeof req.query.index === 'string' ? req.query.index : '0', 10) || 0;
|
||||||
|
const status: string = typeof req.query.status === 'string' ? req.query.status : '';
|
||||||
|
const length = 25;
|
||||||
|
const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, length, status);
|
||||||
|
const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status);
|
||||||
|
res.header('X-Total-Count', channelsCount.toString());
|
||||||
res.json(channels);
|
res.json(channels);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e instanceof Error ? e.message : e);
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user