Channel pagination
This commit is contained in:
		
							parent
							
								
									11a7babbc4
								
							
						
					
					
						commit
						473cb55dc4
					
				| @ -34,7 +34,7 @@ export class AddressLabelsComponent implements OnChanges { | ||||
|   } | ||||
| 
 | ||||
|   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() { | ||||
|  | ||||
| @ -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"> | ||||
|     <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-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="tableHeader"></ng-container> | ||||
|     <tbody> | ||||
|       <tr *ngFor="let channel of response.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> | ||||
|       </tr> | ||||
|     </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> | ||||
|    | ||||
|   <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> | ||||
|    | ||||
| <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"> | ||||
|   <td class="alias text-left"> | ||||
| @ -50,7 +45,7 @@ | ||||
|       <app-clipboard [text]="node.public_key" size="small"></app-clipboard> | ||||
|     </div> | ||||
|   </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"><app-amount [satoshis]="node.capacity" digitsInfo="1.2-2"></app-amount></div> | ||||
|   </td> | ||||
| @ -65,7 +60,37 @@ | ||||
|   <td class="capacity text-right d-none d-md-table-cell"> | ||||
|     <app-amount [satoshis]="channel.capacity" digitsInfo="1.2-2"></app-amount> | ||||
|   </td> | ||||
|   <td class="capacity text-left"> | ||||
|   <td class="capacity text-right"> | ||||
|     <a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.short_id }}</a> | ||||
|    </td> | ||||
| </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 { 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'; | ||||
| 
 | ||||
| @Component({ | ||||
| @ -8,16 +10,53 @@ import { LightningApiService } from '../lightning-api.service'; | ||||
|   styleUrls: ['./channels-list.component.scss'], | ||||
|   changeDetection: ChangeDetectionStrategy.OnPush, | ||||
| }) | ||||
| export class ChannelsListComponent implements OnChanges { | ||||
| export class ChannelsListComponent implements OnInit, OnChanges { | ||||
|   @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( | ||||
|     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 { | ||||
|     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); | ||||
|   } | ||||
| 
 | ||||
|   getChannelsByNodeId$(publicKey: string): Observable<any> { | ||||
|   getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> { | ||||
|     let params = new HttpParams() | ||||
|       .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> { | ||||
|  | ||||
| @ -88,7 +88,6 @@ | ||||
|     </div> | ||||
|      | ||||
|     <br> | ||||
|     <h2>Channels</h2> | ||||
| 
 | ||||
|     <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 { | ||||
|       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`; | ||||
|       const [rows]: any = await DB.query(query, [public_key, public_key]); | ||||
|       // Default active and inactive channels
 | ||||
|       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)); | ||||
|       return channels; | ||||
|     } 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 { | ||||
|     return { | ||||
|       'id': channel.id, | ||||
|       'short_id': channel.short_id, | ||||
|       'capacity': channel.capacity, | ||||
|       'transaction_id': channel.transaction_id, | ||||
|       'transaction_vout': channel.void, | ||||
|       'transaction_vout': channel.transaction_vout, | ||||
|       'updated_at': channel.updated_at, | ||||
|       'created': channel.created, | ||||
|       '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/search/:search', this.$searchChannelsById) | ||||
|       .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 { | ||||
|       if (typeof req.query.public_key !== 'string') { | ||||
|         res.status(501).send('Missing parameter: public_key'); | ||||
|         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); | ||||
|     } catch (e) { | ||||
|       res.status(500).send(e instanceof Error ? e.message : e); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user