diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 77c397ed0..dce48d646 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -13,6 +13,7 @@ import { AssetComponent } from './components/asset/asset.component'; import { AssetsComponent } from './assets/assets.component'; import { StatusViewComponent } from './components/status-view/status-view.component'; import { DashboardComponent } from './dashboard/dashboard.component'; +import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component'; const routes: Routes = [ { @@ -41,6 +42,10 @@ const routes: Routes = [ }, ], }, + { + path: 'blocks', + component: LatestBlocksComponent, + }, { path: 'graphs', component: StatisticsComponent, @@ -85,6 +90,10 @@ const routes: Routes = [ }, ], }, + { + path: 'blocks', + component: LatestBlocksComponent, + }, { path: 'graphs', component: StatisticsComponent, @@ -150,6 +159,10 @@ const routes: Routes = [ }, ], }, + { + path: 'blocks', + component: LatestBlocksComponent, + }, { path: 'graphs', component: StatisticsComponent, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index fe5952f4f..b4562bde8 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -16,6 +16,7 @@ import { StateService } from './services/state.service'; import { BlockComponent } from './components/block/block.component'; import { AddressComponent } from './components/address/address.component'; import { SearchFormComponent } from './components/search-form/search-form.component'; +import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component'; import { WebsocketService } from './services/websocket.service'; import { AddressLabelsComponent } from './components/address-labels/address-labels.component'; import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component'; @@ -41,6 +42,8 @@ import { SharedModule } from './shared/shared.module'; import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; import { FeesBoxComponent } from './components/fees-box/fees-box.component'; import { DashboardComponent } from './dashboard/dashboard.component'; +import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; +import { faChartArea, faCube, faDatabase, faInfo, faInfoCircle, faList, faQuestion, faQuestionCircle, faTachometerAlt, faThList, faTv } from '@fortawesome/free-solid-svg-icons'; @NgModule({ declarations: [ @@ -57,6 +60,7 @@ import { DashboardComponent } from './dashboard/dashboard.component'; TransactionsListComponent, AddressComponent, AmountComponent, + LatestBlocksComponent, SearchFormComponent, TimespanComponent, AddressLabelsComponent, @@ -80,6 +84,7 @@ import { DashboardComponent } from './dashboard/dashboard.component'; BrowserAnimationsModule, InfiniteScrollModule, NgbTypeaheadModule, + FontAwesomeModule, SharedModule, ], providers: [ @@ -91,4 +96,15 @@ import { DashboardComponent } from './dashboard/dashboard.component'; ], bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule { + constructor(library: FaIconLibrary) { + library.addIcons(faInfoCircle); + library.addIcons(faChartArea); + library.addIcons(faTv); + library.addIcons(faCube); + library.addIcons(faThList); + library.addIcons(faList); + library.addIcons(faTachometerAlt); + library.addIcons(faDatabase); + } +} diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.html b/frontend/src/app/components/latest-blocks/latest-blocks.component.html new file mode 100644 index 000000000..e6cafe69d --- /dev/null +++ b/frontend/src/app/components/latest-blocks/latest-blocks.component.html @@ -0,0 +1,40 @@ +
+

Blocks

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
HeightTimestampMinedTransactionsFilled
{{ block.height }}{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} ago{{ block.tx_count | number }} +
+
+
{{ block.size | bytes: 2 }}
+
+
+ +
diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.scss b/frontend/src/app/components/latest-blocks/latest-blocks.component.scss new file mode 100644 index 000000000..0f2246c99 --- /dev/null +++ b/frontend/src/app/components/latest-blocks/latest-blocks.component.scss @@ -0,0 +1,14 @@ +.progress { + background-color: #2d3348; +} + +@media (min-width: 768px) { + .d-md-block { + display: table-cell !important; + } +} +@media (min-width: 992px) { + .d-lg-block { + display: table-cell !important; + } +} diff --git a/frontend/src/app/components/latest-blocks/latest-blocks.component.ts b/frontend/src/app/components/latest-blocks/latest-blocks.component.ts new file mode 100644 index 000000000..c773c91b9 --- /dev/null +++ b/frontend/src/app/components/latest-blocks/latest-blocks.component.ts @@ -0,0 +1,115 @@ +import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; +import { ElectrsApiService } from '../../services/electrs-api.service'; +import { StateService } from '../../services/state.service'; +import { Block } from '../../interfaces/electrs.interface'; +import { Subscription, Observable, merge, of } from 'rxjs'; +import { SeoService } from '../../services/seo.service'; +import { WebsocketService } from 'src/app/services/websocket.service'; + +@Component({ + selector: 'app-latest-blocks', + templateUrl: './latest-blocks.component.html', + styleUrls: ['./latest-blocks.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LatestBlocksComponent implements OnInit, OnDestroy { + network$: Observable; + + blocks: any[] = []; + blockSubscription: Subscription; + isLoading = true; + interval: any; + + latestBlockHeight: number; + + heightOfPageUntilBlocks = 150; + heightOfBlocksTableChunk = 470; + + constructor( + private electrsApiService: ElectrsApiService, + private stateService: StateService, + private seoService: SeoService, + private websocketService: WebsocketService, + private cd: ChangeDetectorRef, + ) { } + + ngOnInit() { + this.seoService.resetTitle(); + this.websocketService.want(['blocks']); + + this.network$ = merge(of(''), this.stateService.networkChanged$); + + this.blockSubscription = this.stateService.blocks$ + .subscribe(([block]) => { + if (block === null || !this.blocks.length) { + return; + } + + this.latestBlockHeight = block.height; + + if (block.height === this.blocks[0].height) { + return; + } + + // If we are out of sync, reload the blocks instead + if (block.height > this.blocks[0].height + 1) { + this.loadInitialBlocks(); + return; + } + + if (block.height <= this.blocks[0].height) { + return; + } + + this.blocks.pop(); + this.blocks.unshift(block); + this.cd.markForCheck(); + }); + + this.loadInitialBlocks(); + } + + ngOnDestroy() { + clearInterval(this.interval); + this.blockSubscription.unsubscribe(); + } + + loadInitialBlocks() { + this.electrsApiService.listBlocks$() + .subscribe((blocks) => { + this.blocks = blocks; + this.isLoading = false; + + this.latestBlockHeight = blocks[0].height; + + const spaceForBlocks = window.innerHeight - this.heightOfPageUntilBlocks; + const chunks = Math.ceil(spaceForBlocks / this.heightOfBlocksTableChunk) - 1; + if (chunks > 0) { + this.loadMore(chunks); + } + this.cd.markForCheck(); + }); + } + + loadMore(chunks = 0) { + if (this.isLoading) { + return; + } + this.isLoading = true; + this.electrsApiService.listBlocks$(this.blocks[this.blocks.length - 1].height - 1) + .subscribe((blocks) => { + this.blocks = this.blocks.concat(blocks); + this.isLoading = false; + + const chunksLeft = chunks - 1; + if (chunksLeft > 0) { + this.loadMore(chunksLeft); + } + this.cd.markForCheck(); + }); + } + + trackByBlock(index: number, block: Block) { + return block.height; + } +} diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index b51c5da5b..5e01662c5 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -28,31 +28,37 @@