Add standalone block visualization page
This commit is contained in:
		
							parent
							
								
									d0696628b2
								
							
						
					
					
						commit
						f3fc774c2d
					
				| @ -4,6 +4,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy' | |||||||
| import { StartComponent } from './components/start/start.component'; | import { StartComponent } from './components/start/start.component'; | ||||||
| import { TransactionComponent } from './components/transaction/transaction.component'; | import { TransactionComponent } from './components/transaction/transaction.component'; | ||||||
| import { BlockComponent } from './components/block/block.component'; | import { BlockComponent } from './components/block/block.component'; | ||||||
|  | import { BlockViewComponent } from './components/block-view/block-view.component'; | ||||||
| import { ClockComponent } from './components/clock/clock.component'; | import { ClockComponent } from './components/clock/clock.component'; | ||||||
| import { AddressComponent } from './components/address/address.component'; | import { AddressComponent } from './components/address/address.component'; | ||||||
| import { MasterPageComponent } from './components/master-page/master-page.component'; | import { MasterPageComponent } from './components/master-page/master-page.component'; | ||||||
| @ -373,6 +374,10 @@ let routes: Routes = [ | |||||||
|     path: 'clock/:mode/:index', |     path: 'clock/:mode/:index', | ||||||
|     component: ClockComponent, |     component: ClockComponent, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     path: 'view/block/:id', | ||||||
|  |     component: BlockViewComponent, | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     path: 'status', |     path: 'status', | ||||||
|     data: { networks: ['bitcoin', 'liquid'] }, |     data: { networks: ['bitcoin', 'liquid'] }, | ||||||
|  | |||||||
| @ -0,0 +1,13 @@ | |||||||
|  | <div class="block-wrapper"> | ||||||
|  |   <div class="block-container"> | ||||||
|  |     <app-block-overview-graph | ||||||
|  |       #blockGraph | ||||||
|  |       [isLoading]="false" | ||||||
|  |       [resolution]="resolution" | ||||||
|  |       [blockLimit]="stateService.blockVSize" | ||||||
|  |       [orientation]="'top'" | ||||||
|  |       [flip]="false" | ||||||
|  |       [disableSpinner]="true" | ||||||
|  |     ></app-block-overview-graph> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
| @ -0,0 +1,22 @@ | |||||||
|  | .block-wrapper { | ||||||
|  |   width: 100vw; | ||||||
|  |   height: 100vh; | ||||||
|  |   background: #181b2d; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .block-container { | ||||||
|  |   flex-grow: 0; | ||||||
|  |   flex-shrink: 0; | ||||||
|  |   width: 100vw; | ||||||
|  |   max-width: 100vh; | ||||||
|  |   height: 100vh; | ||||||
|  |   padding: 0; | ||||||
|  |   margin: auto; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  | 
 | ||||||
|  |   * { | ||||||
|  |     flex-grow: 1; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										176
									
								
								frontend/src/app/components/block-view/block-view.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								frontend/src/app/components/block-view/block-view.component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,176 @@ | |||||||
|  | import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core'; | ||||||
|  | import { ActivatedRoute, ParamMap } from '@angular/router'; | ||||||
|  | import { ElectrsApiService } from '../../services/electrs-api.service'; | ||||||
|  | import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators'; | ||||||
|  | import { of, Subscription, asyncScheduler } from 'rxjs'; | ||||||
|  | import { StateService } from '../../services/state.service'; | ||||||
|  | import { SeoService } from '../../services/seo.service'; | ||||||
|  | import { OpenGraphService } from '../../services/opengraph.service'; | ||||||
|  | import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface'; | ||||||
|  | import { ApiService } from '../../services/api.service'; | ||||||
|  | import { seoDescriptionNetwork } from '../../shared/common.utils'; | ||||||
|  | import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component'; | ||||||
|  | 
 | ||||||
|  | function bestFitResolution(min, max, n) { | ||||||
|  |   const target = (min + max) / 2; | ||||||
|  |   let bestScore = Infinity; | ||||||
|  |   let best = null; | ||||||
|  |   for (let i = min; i <= max; i++) { | ||||||
|  |     const remainder = (n % i); | ||||||
|  |     if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) { | ||||||
|  |       bestScore = remainder; | ||||||
|  |       best = i; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return best; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   selector: 'app-block-view', | ||||||
|  |   templateUrl: './block-view.component.html', | ||||||
|  |   styleUrls: ['./block-view.component.scss'] | ||||||
|  | }) | ||||||
|  | export class BlockViewComponent implements OnInit, OnDestroy { | ||||||
|  |   network = ''; | ||||||
|  |   block: BlockExtended; | ||||||
|  |   blockHeight: number; | ||||||
|  |   blockHash: string; | ||||||
|  |   rawId: string; | ||||||
|  |   isLoadingBlock = true; | ||||||
|  |   strippedTransactions: TransactionStripped[]; | ||||||
|  |   isLoadingOverview = true; | ||||||
|  |   autofit: boolean = false; | ||||||
|  |   resolution: number = 80; | ||||||
|  | 
 | ||||||
|  |   overviewSubscription: Subscription; | ||||||
|  |   networkChangedSubscription: Subscription; | ||||||
|  |   queryParamsSubscription: Subscription; | ||||||
|  | 
 | ||||||
|  |   @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; | ||||||
|  | 
 | ||||||
|  |   constructor( | ||||||
|  |     private route: ActivatedRoute, | ||||||
|  |     private electrsApiService: ElectrsApiService, | ||||||
|  |     public stateService: StateService, | ||||||
|  |     private seoService: SeoService, | ||||||
|  |     private openGraphService: OpenGraphService, | ||||||
|  |     private apiService: ApiService | ||||||
|  |   ) { } | ||||||
|  | 
 | ||||||
|  |   ngOnInit() { | ||||||
|  |     this.network = this.stateService.network; | ||||||
|  | 
 | ||||||
|  |     this.queryParamsSubscription = this.route.queryParams.subscribe((params) => { | ||||||
|  |       this.autofit = params.autofit === 'true'; | ||||||
|  |       if (this.autofit) { | ||||||
|  |         this.onResize(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const block$ = this.route.paramMap.pipe( | ||||||
|  |       switchMap((params: ParamMap) => { | ||||||
|  |         this.rawId = params.get('id') || ''; | ||||||
|  | 
 | ||||||
|  |         const blockHash: string = params.get('id') || ''; | ||||||
|  |         this.block = undefined; | ||||||
|  | 
 | ||||||
|  |         let isBlockHeight = false; | ||||||
|  |         if (/^[0-9]+$/.test(blockHash)) { | ||||||
|  |           isBlockHeight = true; | ||||||
|  |         } else { | ||||||
|  |           this.blockHash = blockHash; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.isLoadingBlock = true; | ||||||
|  |         this.isLoadingOverview = true; | ||||||
|  | 
 | ||||||
|  |         if (isBlockHeight) { | ||||||
|  |           return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10)) | ||||||
|  |             .pipe( | ||||||
|  |               switchMap((hash) => { | ||||||
|  |                 if (hash) { | ||||||
|  |                   this.blockHash = hash; | ||||||
|  |                   return this.apiService.getBlock$(hash); | ||||||
|  |                 } else { | ||||||
|  |                   return null; | ||||||
|  |                 } | ||||||
|  |               }), | ||||||
|  |               catchError(() => { | ||||||
|  |                 return of(null); | ||||||
|  |               }), | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |         return this.apiService.getBlock$(blockHash); | ||||||
|  |       }), | ||||||
|  |       filter((block: BlockExtended | void) => block != null), | ||||||
|  |       tap((block: BlockExtended) => { | ||||||
|  |         this.block = block; | ||||||
|  |         this.blockHeight = block.height; | ||||||
|  | 
 | ||||||
|  |         this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`); | ||||||
|  |         if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) { | ||||||
|  |           this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`); | ||||||
|  |         } else { | ||||||
|  |           this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`); | ||||||
|  |         } | ||||||
|  |         this.isLoadingBlock = false; | ||||||
|  |         this.isLoadingOverview = true; | ||||||
|  |       }), | ||||||
|  |       shareReplay(1) | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     this.overviewSubscription = block$.pipe( | ||||||
|  |       switchMap((block) => this.apiService.getStrippedBlockTransactions$(block.id) | ||||||
|  |         .pipe( | ||||||
|  |           catchError(() => { | ||||||
|  |             return of([]); | ||||||
|  |           }), | ||||||
|  |           switchMap((transactions) => { | ||||||
|  |             return of(transactions); | ||||||
|  |           }) | ||||||
|  |         ) | ||||||
|  |       ), | ||||||
|  |     ) | ||||||
|  |     .subscribe((transactions: TransactionStripped[]) => { | ||||||
|  |       this.strippedTransactions = transactions; | ||||||
|  |       this.isLoadingOverview = false; | ||||||
|  |       if (this.blockGraph) { | ||||||
|  |         this.blockGraph.destroy(); | ||||||
|  |         this.blockGraph.setup(this.strippedTransactions); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     () => { | ||||||
|  |       this.isLoadingOverview = false; | ||||||
|  |       if (this.blockGraph) { | ||||||
|  |         this.blockGraph.destroy(); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     this.networkChangedSubscription = this.stateService.networkChanged$ | ||||||
|  |       .subscribe((network) => this.network = network); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @HostListener('window:resize', ['$event']) | ||||||
|  |   onResize(): void { | ||||||
|  |     if (this.autofit) { | ||||||
|  |       this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight)); | ||||||
|  |       console.log('resized, new resolution ', this.resolution, window.innerWidth, window.innerHeight); | ||||||
|  |       // if (this.blockGraph && this.strippedTransactions) {
 | ||||||
|  |       //   this.blockGraph.destroy();
 | ||||||
|  |       //   this.blockGraph.setup(this.strippedTransactions);
 | ||||||
|  |       // }
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnDestroy() { | ||||||
|  |     if (this.overviewSubscription) { | ||||||
|  |       this.overviewSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|  |     if (this.networkChangedSubscription) { | ||||||
|  |       this.networkChangedSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|  |     if (this.queryParamsSubscription) { | ||||||
|  |       this.queryParamsSubscription.unsubscribe(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -97,6 +97,7 @@ import { AcceleratePreviewComponent } from '../components/accelerate-preview/acc | |||||||
| import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/accelerate-fee-graph.component'; | import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/accelerate-fee-graph.component'; | ||||||
| import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component'; | import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component'; | ||||||
| 
 | 
 | ||||||
|  | import { BlockViewComponent } from '../components/block-view/block-view.component'; | ||||||
| import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; | import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component'; | ||||||
| import { ClockchainComponent } from '../components/clockchain/clockchain.component'; | import { ClockchainComponent } from '../components/clockchain/clockchain.component'; | ||||||
| import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; | import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; | ||||||
| @ -134,6 +135,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | |||||||
|     FiatCurrencyPipe, |     FiatCurrencyPipe, | ||||||
|     ColoredPriceDirective, |     ColoredPriceDirective, | ||||||
|     BlockchainComponent, |     BlockchainComponent, | ||||||
|  |     BlockViewComponent, | ||||||
|     MempoolBlocksComponent, |     MempoolBlocksComponent, | ||||||
|     BlockchainBlocksComponent, |     BlockchainBlocksComponent, | ||||||
|     AmountComponent, |     AmountComponent, | ||||||
| @ -196,6 +198,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | |||||||
|     AccelerateFeeGraphComponent, |     AccelerateFeeGraphComponent, | ||||||
|     CalculatorComponent, |     CalculatorComponent, | ||||||
|     BitcoinsatoshisPipe, |     BitcoinsatoshisPipe, | ||||||
|  |     BlockViewComponent, | ||||||
|     MempoolBlockOverviewComponent, |     MempoolBlockOverviewComponent, | ||||||
|     ClockchainComponent, |     ClockchainComponent, | ||||||
|     ClockComponent, |     ClockComponent, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user