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