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 { TransactionComponent } from './components/transaction/transaction.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 { AddressComponent } from './components/address/address.component'; | ||||
| import { MasterPageComponent } from './components/master-page/master-page.component'; | ||||
| @ -373,6 +374,10 @@ let routes: Routes = [ | ||||
|     path: 'clock/:mode/:index', | ||||
|     component: ClockComponent, | ||||
|   }, | ||||
|   { | ||||
|     path: 'view/block/:id', | ||||
|     component: BlockViewComponent, | ||||
|   }, | ||||
|   { | ||||
|     path: 'status', | ||||
|     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 { 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 { ClockchainComponent } from '../components/clockchain/clockchain.component'; | ||||
| import { ClockFaceComponent } from '../components/clock-face/clock-face.component'; | ||||
| @ -134,6 +135,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     FiatCurrencyPipe, | ||||
|     ColoredPriceDirective, | ||||
|     BlockchainComponent, | ||||
|     BlockViewComponent, | ||||
|     MempoolBlocksComponent, | ||||
|     BlockchainBlocksComponent, | ||||
|     AmountComponent, | ||||
| @ -196,6 +198,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir | ||||
|     AccelerateFeeGraphComponent, | ||||
|     CalculatorComponent, | ||||
|     BitcoinsatoshisPipe, | ||||
|     BlockViewComponent, | ||||
|     MempoolBlockOverviewComponent, | ||||
|     ClockchainComponent, | ||||
|     ClockComponent, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user