diff --git a/src/app/modules/dashboard/components/validator-performance-list/validator-performance-list.component.html b/src/app/modules/dashboard/components/validator-performance-list/validator-performance-list.component.html index ec7474a9..2e1e416e 100644 --- a/src/app/modules/dashboard/components/validator-performance-list/validator-performance-list.component.html +++ b/src/app/modules/dashboard/components/validator-performance-list/validator-performance-list.component.html @@ -1,10 +1,13 @@ + [hasError]="hasError" [errorMessage]="'No validator performance information + available'" + [noData]="noData" [noDataMessage]="'No validator performance information + available'"> + [theme]="{ 'border-radius': '6px', height: '20px', 'margin-top': + '10px'}"> @@ -18,13 +21,15 @@ Last included slot - {{element.inclusionSlots | epoch}} + {{element.inclusionSlots | + epoch}} Correctly voted source - + @@ -39,7 +44,8 @@ Voted target - + @@ -47,7 +53,11 @@ Voted head - + @@ -56,5 +66,8 @@ - + diff --git a/src/app/modules/dashboard/components/validator-performance-list/validator-performance-list.component.ts b/src/app/modules/dashboard/components/validator-performance-list/validator-performance-list.component.ts index bd0530d6..3bfdf732 100644 --- a/src/app/modules/dashboard/components/validator-performance-list/validator-performance-list.component.ts +++ b/src/app/modules/dashboard/components/validator-performance-list/validator-performance-list.component.ts @@ -1,13 +1,15 @@ -import { Component, ViewChild } from '@angular/core'; -import { MatPaginator } from '@angular/material/paginator'; +import { Component, ViewChild, OnInit } from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { BigNumber } from 'ethers'; import { ValidatorPerformanceResponse } from 'src/app/proto/eth/v1alpha1/beacon_chain'; import { throwError } from 'rxjs'; -import { catchError, map, take, tap } from 'rxjs/operators'; +import { catchError, map, take, tap, takeUntil, filter } from 'rxjs/operators'; import { ValidatorService } from '../../../core/services/validator.service'; +import { BaseComponent } from '../../../shared/components/base.component'; +import { UserService } from '../../../shared/services/user.service'; export interface ValidatorListItem { publicKey: string; @@ -26,61 +28,87 @@ export interface ValidatorListItem { selector: 'app-validator-performance-list', templateUrl: './validator-performance-list.component.html', }) -export class ValidatorPerformanceListComponent { +export class ValidatorPerformanceListComponent + extends BaseComponent + implements OnInit { displayedColumns: string[] = [ 'publicKey', 'attLastIncludedSlot', 'correctlyVotedSource', 'correctlyVotedTarget', 'correctlyVotedHead', - 'gains' + 'gains', ]; dataSource?: MatTableDataSource; - @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator | null = null; + @ViewChild(MatPaginator, { static: true }) + paginator: MatPaginator | null = null; @ViewChild(MatSort, { static: true }) sort: MatSort | null = null; loading = true; hasError = false; noData = false; - - constructor(private validatorService: ValidatorService) { - this.validatorService.performance$.pipe( - map((performance: ValidatorPerformanceResponse) => { - const list: ValidatorListItem[] = []; - if (performance) { - for (let i = 0; i < performance.publicKeys.length; i++) { - const item = {} as ValidatorListItem; - item.publicKey = performance.publicKeys[i]; - item.correctlyVotedSource = performance.correctlyVotedSource[i]; - item.correctlyVotedHead = performance.correctlyVotedHead[i]; - item.correctlyVotedTarget = performance.correctlyVotedTarget[i]; - item.balancesAfterEpochTransition = performance.balancesAfterEpochTransition[i] || '0'; - item.balancesBeforeEpochTransition = performance.balancesBeforeEpochTransition[i] || '0'; - item.currentEffectiveBalances = performance.currentEffectiveBalances[i] || '0'; - item.inclusionDistances = performance.inclusionDistances[i]; - item.inclusionSlots = performance.inclusionSlots[i]; - item.gains = BigNumber.from(item.balancesAfterEpochTransition).sub( - BigNumber.from(item.balancesBeforeEpochTransition)).toString(); - list.push(item); + pageSizeOptions: number[] = []; + pageSize = 5; + constructor( + private validatorService: ValidatorService, + private userService: UserService + ) { + super(); + this.validatorService.performance$ + .pipe( + map((performance: ValidatorPerformanceResponse) => { + const list: ValidatorListItem[] = []; + if (performance) { + for (let i = 0; i < performance.publicKeys.length; i++) { + const item = {} as ValidatorListItem; + item.publicKey = performance.publicKeys[i]; + item.correctlyVotedSource = performance.correctlyVotedSource[i]; + item.correctlyVotedHead = performance.correctlyVotedHead[i]; + item.correctlyVotedTarget = performance.correctlyVotedTarget[i]; + item.balancesAfterEpochTransition = + performance.balancesAfterEpochTransition[i] || '0'; + item.balancesBeforeEpochTransition = + performance.balancesBeforeEpochTransition[i] || '0'; + item.currentEffectiveBalances = + performance.currentEffectiveBalances[i] || '0'; + item.inclusionDistances = performance.inclusionDistances[i]; + item.inclusionSlots = performance.inclusionSlots[i]; + item.gains = BigNumber.from(item.balancesAfterEpochTransition) + .sub(BigNumber.from(item.balancesBeforeEpochTransition)) + .toString(); + list.push(item); + } } - } - return list; - }), - tap(result => { - this.dataSource = new MatTableDataSource(result); - this.dataSource.paginator = this.paginator; - this.dataSource.sort = this.sort; - this.loading = false; - this.noData = result.length === 0; - }), - catchError(err => { - this.loading = false; - this.hasError = true; - return throwError(err); - }), - take(1) - ).subscribe(); + return list; + }), + tap((result) => { + this.dataSource = new MatTableDataSource(result); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.loading = false; + this.noData = result.length === 0; + }), + catchError((err) => { + this.loading = false; + this.hasError = true; + return throwError(err); + }), + take(1) + ) + .subscribe(); + } + ngOnInit(): void { + this.userService.user$ + .pipe( + takeUntil(this.destroyed$), + filter((x) => !!x), + tap((x) => { + this.pageSizeOptions = x.pageSizeOptions; + this.pageSize = x.gainAndLosesPageSize; + }) + ) + .subscribe(); } applyFilter(event: Event): void { @@ -90,4 +118,8 @@ export class ValidatorPerformanceListComponent { this.dataSource.paginator?.firstPage(); } } + + pageChange(ev: PageEvent): void { + this.userService.changeGainsAndLosesPageSize(ev.pageSize); + } } diff --git a/src/app/modules/shared/services/user.service.spec.ts b/src/app/modules/shared/services/user.service.spec.ts new file mode 100644 index 00000000..3f804c9f --- /dev/null +++ b/src/app/modules/shared/services/user.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { UserService } from './user.service'; + +describe('UserService', () => { + let service: UserService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(UserService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/modules/shared/services/user.service.ts b/src/app/modules/shared/services/user.service.ts new file mode 100644 index 00000000..51e59721 --- /dev/null +++ b/src/app/modules/shared/services/user.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { User } from '../types/user'; + +@Injectable({ + providedIn: 'root', +}) +export class UserService { + constructor() { + this.setUser(this.getUser()); + } + + private userKeyStore = 'user-prysm'; + + private user: User | null | undefined; + private onUserChange = new BehaviorSubject((null as any) as User); + user$ = this.onUserChange.asObservable(); + + changeAccountListPerPage(accountListPerPage: number): void { + if (!this.user) { + return; + } + this.user.acountsPerPage = accountListPerPage; + this.saveChanges(); + } + + changeGainsAndLosesPageSize(pageSize: number): void { + if (!this.user) { + return; + } + this.user.gainAndLosesPageSize = pageSize; + this.saveChanges(); + } + + private saveChanges(): void { + if (this.user) { + localStorage.setItem(this.userKeyStore, JSON.stringify(this.user)); + this.onUserChange.next(this.user); + } + } + + private getUser(): User { + const userStr = localStorage.getItem(this.userKeyStore); + if (!userStr) { + const user = new User(); + return user; + } else { + const user = new User(JSON.parse(userStr)); + return user; + } + } + + private setUser(user: User): void { + this.user = user; + this.saveChanges(); + } +} diff --git a/src/app/modules/shared/types/user.ts b/src/app/modules/shared/types/user.ts new file mode 100644 index 00000000..7a4e305e --- /dev/null +++ b/src/app/modules/shared/types/user.ts @@ -0,0 +1,8 @@ +export class User { + constructor(init?: Partial) { + Object.assign(this, init); + } + acountsPerPage = 5; + gainAndLosesPageSize = 5; + pageSizeOptions: number[] = [5, 10, 50, 100, 250]; +} diff --git a/src/app/modules/wallet/components/accounts-table/accounts-table.component.ts b/src/app/modules/wallet/components/accounts-table/accounts-table.component.ts index 94e9aa43..bc6a61b5 100644 --- a/src/app/modules/wallet/components/accounts-table/accounts-table.component.ts +++ b/src/app/modules/wallet/components/accounts-table/accounts-table.component.ts @@ -10,6 +10,8 @@ import { base64ToHex } from 'src/app/modules/core/utils/hex-util'; import { AccountDeleteComponent } from '../account-delete/account-delete.component'; import { MenuItem } from '../icon-trigger-select/icon-trigger-select.component'; +import { UserService } from '../../../shared/services/user.service'; +import { Router } from '@angular/router'; export interface TableData { select: number; diff --git a/src/app/modules/wallet/pages/accounts/accounts.component.html b/src/app/modules/wallet/pages/accounts/accounts.component.html index 57649ff0..cfad9457 100644 --- a/src/app/modules/wallet/pages/accounts/accounts.component.html +++ b/src/app/modules/wallet/pages/accounts/accounts.component.html @@ -41,6 +41,7 @@ diff --git a/src/app/modules/wallet/pages/accounts/accounts.component.ts b/src/app/modules/wallet/pages/accounts/accounts.component.ts index d25072b2..f1a7c039 100644 --- a/src/app/modules/wallet/pages/accounts/accounts.component.ts +++ b/src/app/modules/wallet/pages/accounts/accounts.component.ts @@ -1,44 +1,61 @@ -import { Component, ViewChild } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatTableDataSource } from '@angular/material/table'; import { SelectionModel } from '@angular/cdk/collections'; import { BigNumber } from 'ethers'; import { BehaviorSubject, Observable, throwError, zip } from 'rxjs'; -import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators'; +import { + catchError, + debounceTime, + map, + share, + switchMap, + tap, +} from 'rxjs/operators'; import { zipMap } from 'rxjs-pipe-ext/lib'; import { base64ToHex } from 'src/app/modules/core/utils/hex-util'; import { ValidatorService } from 'src/app/modules/core/services/validator.service'; import { WalletService } from '../../../core/services/wallet.service'; -import { FAR_FUTURE_EPOCH, GWEI_PER_ETHER } from 'src/app/modules/core/constants'; +import { + FAR_FUTURE_EPOCH, + GWEI_PER_ETHER, +} from 'src/app/modules/core/constants'; import { ValidatorBalances, Validators, Validators_ValidatorContainer, } from 'src/app/proto/eth/v1alpha1/beacon_chain'; -import { - Validator -} from 'src/app/proto/eth/v1alpha1/validator'; +import { Validator } from 'src/app/proto/eth/v1alpha1/validator'; import { ListAccountsResponse } from 'src/app/proto/validator/accounts/v2/web_api'; import { TableData } from '../../components/accounts-table/accounts-table.component'; import { formatUnits } from 'ethers/lib/utils'; +import { Router } from '@angular/router'; +import { UserService } from 'src/app/modules/shared/services/user.service'; +import { BaseComponent } from '../../../shared/components/base.component'; +import { filter, takeUntil } from 'rxjs/operators'; @Component({ selector: 'app-accounts', templateUrl: './accounts.component.html', }) -export class AccountsComponent { - @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator | null = null; +export class AccountsComponent extends BaseComponent implements OnInit { + @ViewChild(MatPaginator, { static: true }) + paginator: MatPaginator | null = null; constructor( private walletService: WalletService, - private validatorService: ValidatorService, - ) { } + private userService: UserService, + private validatorService: ValidatorService + ) { + super(); + } pageSizes: number[] = [5, 10, 50, 100, 250]; totalData = 0; loading = false; + pageSize = 5; // Observables. private pageChanged$ = new BehaviorSubject({ @@ -46,34 +63,55 @@ export class AccountsComponent { pageSize: this.pageSizes[0], } as PageEvent); selection = new SelectionModel(true /* allow multiselect */, []); - tableDataSource$: Observable> = this.pageChanged$.pipe( + tableDataSource$: Observable< + MatTableDataSource + > = this.pageChanged$.pipe( // Debounce to prevent spamming the paginator component. - tap(() => this.loading = true), + tap(() => (this.loading = true)), debounceTime(300), - switchMap((ev: PageEvent) => this.walletService.accounts(ev.pageIndex, ev.pageSize).pipe( - // Extract the validating public keys. - zipMap(accs => accs.accounts?.map(account => account.validatingPublicKey)), - switchMap(([accountsResponse, pubKeys]) => - // Combine the list of validators and their balances to display in the table. - zip( - this.validatorService.validatorList(pubKeys, 0, pubKeys.length), - this.validatorService.balances(pubKeys, 0, pubKeys.length) - ).pipe( - map(([validators, balances]) => - // Transform the data into a pretty format for our table. - this.transformTableData(accountsResponse, validators, balances) - ), + switchMap((ev: PageEvent) => { + return this.walletService.accounts(ev.pageIndex, ev.pageSize).pipe( + // Extract the validating public keys. + zipMap((accs) => + accs.accounts?.map((account) => account.validatingPublicKey) + ), + switchMap(([accountsResponse, pubKeys]) => + // Combine the list of validators and their balances to display in the table. + zip( + this.validatorService.validatorList(pubKeys, 0, pubKeys.length), + this.validatorService.balances(pubKeys, 0, pubKeys.length) + ).pipe( + map(([validators, balances]) => + // Transform the data into a pretty format for our table. + this.transformTableData(accountsResponse, validators, balances) + ) + ) ) - ), - )), + ); + }), share(), // Share the observable across all subscribers. - tap(() => this.loading = false), - catchError(err => { + tap(() => (this.loading = false)), + catchError((err) => { return throwError(err); - }), + }) ); - - applySearchFilter(event: Event, dataSource: MatTableDataSource): void { + ngOnInit(): void { + this.userService.user$ + .pipe( + filter((x) => !!x), + takeUntil(this.destroyed$), + tap((x) => { + this.pageSize = x.acountsPerPage; + this.pageSizes = x.pageSizeOptions; + console.log(x.acountsPerPage); + }) + ) + .subscribe(); + } + applySearchFilter( + event: Event, + dataSource: MatTableDataSource + ): void { const filterValue = (event.target as HTMLInputElement).value; if (dataSource) { dataSource.filter = filterValue.trim().toLowerCase(); @@ -83,6 +121,7 @@ export class AccountsComponent { // Event handles. handlePageEvent(event: PageEvent): void { + this.userService.changeAccountListPerPage(event.pageSize); this.pageChanged$.next(event); } @@ -94,7 +133,7 @@ export class AccountsComponent { this.totalData = accountsResponse.totalSize; const tableData = accountsResponse.accounts.map((acc, idx) => { let val = validators?.validatorList?.find( - v => acc.validatingPublicKey === v?.validator?.publicKey + (v) => acc.validatingPublicKey === v?.validator?.publicKey ); if (!val) { val = { @@ -103,17 +142,24 @@ export class AccountsComponent { effectiveBalance: '0', activationEpoch: FAR_FUTURE_EPOCH, exitEpoch: FAR_FUTURE_EPOCH, - } as Validator + } as Validator, } as Validators_ValidatorContainer; } - const balanceItem = balances?.balances.find(b => b.publicKey === acc.validatingPublicKey); + const balanceItem = balances?.balances.find( + (b) => b.publicKey === acc.validatingPublicKey + ); let bal = '0'; let status = 'unknown'; if (balanceItem && balanceItem.status) { - status = balanceItem.status !== '' ? balanceItem.status.toLowerCase() : 'unknown'; + status = + balanceItem.status !== '' + ? balanceItem.status.toLowerCase() + : 'unknown'; bal = formatUnits(BigNumber.from(balanceItem.balance), 'gwei'); } - const effectiveBalance = BigNumber.from(val?.validator?.effectiveBalance).div(GWEI_PER_ETHER); + const effectiveBalance = BigNumber.from( + val?.validator?.effectiveBalance + ).div(GWEI_PER_ETHER); return { select: idx, accountName: acc?.accountName, @@ -133,10 +179,10 @@ export class AccountsComponent { return dataSource; } - private filterPredicate(el: TableData, filter: string): boolean { - const inAccountName = el.accountName.indexOf(filter) !== -1; - const inPublicKey = base64ToHex(el.publicKey).indexOf(filter) !== -1; - const inValidatorIndex = el.index.toString() === filter; + private filterPredicate(el: TableData, f: string): boolean { + const inAccountName = el.accountName.indexOf(f) !== -1; + const inPublicKey = base64ToHex(el.publicKey).indexOf(f) !== -1; + const inValidatorIndex = el.index.toString() === f; return inAccountName || inPublicKey || inValidatorIndex; } }