Channel pagination
This commit is contained in:
		
							parent
							
								
									7f7fa2eb6a
								
							
						
					
					
						commit
						6e2b1d633b
					
				@ -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