Merge pull request #3831 from mempool/mononaut/clock

Interactive clock
This commit is contained in:
softsimon 2023-06-14 22:14:26 +02:00 committed by GitHub
commit dcf73ec3f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 115 additions and 60 deletions

View File

@ -4,8 +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 { ClockMinedComponent as ClockMinedComponent } from './components/clock/clock-mined.component';
import { ClockMempoolComponent as ClockMempoolComponent } from './components/clock/clock-mempool.component';
import { ClockComponent } from './components/clock/clock.component';
import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { AboutComponent } from './components/about/about.component';
@ -358,12 +357,16 @@ let routes: Routes = [
],
},
{
path: 'clock-mined',
component: ClockMinedComponent,
path: 'clock',
redirectTo: 'clock/mempool/0'
},
{
path: 'clock-mempool',
component: ClockMempoolComponent,
path: 'clock/:mode',
redirectTo: 'clock/:mode/0'
},
{
path: 'clock/:mode/:index',
component: ClockComponent,
},
{
path: 'status',

View File

@ -13,10 +13,10 @@
[class.offscreen]="!static && count && i >= count"
id="bitcoin-block-{{ block.height }}" [ngStyle]="blockStyles[i]"
[class.blink-bg]="isSpecial(block.height)">
<a draggable="false" [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }"
<a draggable="false" [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div *ngIf="!minimal" [attr.data-cy]="'bitcoin-block-' + i + '-height'" class="block-height">
<a [routerLink]="['/block/' | relativeUrl, block.id]" [state]="{ data: { block: block } }">{{ block.height
<a [routerLink]="[getHref(i, block) | relativeUrl]" [state]="{ data: { block: block } }">{{ block.height
}}</a>
</div>
<div class="block-body">

View File

@ -27,6 +27,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() minimal: boolean = false;
@Input() blockWidth: number = 125;
@Input() spotlight: number = 0;
@Input() getHref?: (index, block) => string = (index, block) => `/block/${block.id}`;
specialBlocks = specialBlocks;
network = '';

View File

@ -1 +0,0 @@
<app-clock mode="mempool"></app-clock>

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-clock-mempool',
templateUrl: './clock-mempool.component.html',
})
export class ClockMempoolComponent {}

View File

@ -1 +0,0 @@
<app-clock mode="block"></app-clock>

View File

@ -1,7 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-clock-mined',
templateUrl: './clock-mined.component.html',
})
export class ClockMinedComponent {}

View File

@ -1,14 +1,19 @@
<div class="clock-wrapper" [style]="wrapperStyle">
<div class="clockchain-bar" [style.height]="chainHeight + 'px'">
<div class="clockchain">
<app-clockchain [width]="chainWidth" [height]="chainHeight" [mode]="mode"></app-clockchain>
<app-clockchain
[width]="chainWidth"
[height]="chainHeight"
[mode]="mode"
[index]="blockIndex"
></app-clockchain>
</div>
</div>
<div class="clock-face">
<app-clock-face [size]="clockSize">
<div class="block-wrapper">
<ng-container *ngIf="block && block.height >= 0">
<ng-container *ngIf="mode === 'block'; else mempoolMode;">
<ng-container *ngIf="blocks && blocks.length">
<ng-container *ngIf="mode === 'mined'; else mempoolMode;">
<div class="block-cube">
<div class="side top"></div>
<div class="side bottom"></div>
@ -20,12 +25,12 @@
</ng-container>
<ng-template #mempoolMode>
<div class="block-sizer" [style]="blockSizerStyle">
<app-mempool-block-overview [index]="0" [pixelAlign]="true"></app-mempool-block-overview>
<app-mempool-block-overview [index]="blockIndex" [pixelAlign]="true"></app-mempool-block-overview>
</div>
</ng-template>
<div class="fader"></div>
<div class="title-wrapper">
<h1 class="block-height">{{ block.height }}</h1>
<h1 class="block-height">{{ blocks[mode === 'mempool' ? 0 : blockIndex].height }}</h1>
</div>
</ng-container>
</div>
@ -42,13 +47,13 @@
<p class="label" i18n="clock.priority-rate|priority fee rate">priority rate</p>
<p *ngIf="recommendedFees$ | async as recommendedFees;" i18n="shared.sat-vbyte|sat/vB">{{ recommendedFees.fastestFee }} sat/vB</p>
</div>
<div *ngIf="mode !== 'mempool' && block" class="stats bottom left">
<p [innerHTML]="block.size | bytes: 2"></p>
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom left">
<p [innerHTML]="blocks[blockIndex].size | bytes: 2"></p>
<p class="label" i18n="clock.block-size">block size</p>
</div>
<div *ngIf="mode !== 'mempool' && block" class="stats bottom right">
<div *ngIf="mode !== 'mempool' && blocks?.length" class="stats bottom right">
<p class="force-wrap">
<ng-container *ngTemplateOutlet="block.tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.tx_count | number}"></ng-container>
<ng-container *ngTemplateOutlet="blocks[blockIndex].tx_count === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: blocks[blockIndex].tx_count | number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} <span class="label">transaction</span></ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} <span class="label">transactions</span></ng-template>
</p>

View File

@ -1,10 +1,11 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, Input, OnInit } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { Observable, Subscription, of, switchMap, tap } from 'rxjs';
import { StateService } from '../../services/state.service';
import { BlockExtended } from '../../interfaces/node-api.interface';
import { WebsocketService } from '../../services/websocket.service';
import { MempoolInfo, Recommendedfees } from '../../interfaces/websocket.interface';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
@Component({
selector: 'app-clock',
@ -13,12 +14,14 @@ import { ActivatedRoute } from '@angular/router';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ClockComponent implements OnInit {
@Input() mode: 'block' | 'mempool' = 'block';
hideStats: boolean = false;
mode: 'mempool' | 'mined' = 'mined';
blockIndex: number;
pageSubscription: Subscription;
blocksSubscription: Subscription;
recommendedFees$: Observable<Recommendedfees>;
mempoolInfo$: Observable<MempoolInfo>;
block: BlockExtended;
blocks: BlockExtended[] = [];
clockSize: number = 300;
chainWidth: number = 384;
chainHeight: number = 60;
@ -41,6 +44,8 @@ export class ClockComponent implements OnInit {
public stateService: StateService,
private websocketService: WebsocketService,
private route: ActivatedRoute,
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
private cd: ChangeDetectorRef,
) {
this.route.queryParams.subscribe((params) => {
@ -57,14 +62,40 @@ export class ClockComponent implements OnInit {
this.blocksSubscription = this.stateService.blocks$
.subscribe(([block]) => {
if (block) {
this.block = block;
this.blockStyle = this.getStyleForBlock(this.block);
this.cd.markForCheck();
this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, 16);
if (this.blocks[this.blockIndex]) {
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
this.cd.markForCheck();
}
}
});
this.recommendedFees$ = this.stateService.recommendedFees$;
this.mempoolInfo$ = this.stateService.mempoolInfo$;
this.pageSubscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
const rawMode: string = params.get('mode');
const mode = rawMode === 'mempool' ? 'mempool' : 'mined';
const index: number = Number.parseInt(params.get('index'));
if (mode !== rawMode || index < 0 || isNaN(index)) {
this.router.navigate([this.relativeUrlPipe.transform('/clock'), mode, index || 0]);
}
return of({
mode,
index,
});
}),
tap((page: { mode: 'mempool' | 'mined', index: number }) => {
this.mode = page.mode;
this.blockIndex = page.index || 0;
if (this.blocks[this.blockIndex]) {
this.blockStyle = this.getStyleForBlock(this.blocks[this.blockIndex]);
this.cd.markForCheck();
}
})
).subscribe();
}
getStyleForBlock(block: BlockExtended) {

View File

@ -5,8 +5,20 @@
<div class="position-container" [ngClass]="network ? network : ''" [style.top]="(height / 3) + 'px'">
<span>
<div class="blocks-wrapper">
<app-mempool-blocks [minimal]="true" [count]="mempoolBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'mempool' ? 1 : 0"></app-mempool-blocks>
<app-blockchain-blocks [minimal]="true" [count]="blockchainBlocks" [blockWidth]="blockWidth" [spotlight]="mode === 'block' ? -1 : 0"></app-blockchain-blocks>
<app-mempool-blocks
[minimal]="true"
[count]="mempoolBlocks"
[blockWidth]="blockWidth"
[spotlight]="mode === 'mempool' ? index + 1 : 0"
[getHref]="getMempoolUrl"
></app-mempool-blocks>
<app-blockchain-blocks
[minimal]="true"
[count]="blockchainBlocks"
[blockWidth]="blockWidth"
[spotlight]="mode === 'mined' ? -index - 1 : 0"
[getHref]="getMinedUrl"
></app-blockchain-blocks>
</div>
<div class="divider" [style.top]="-(height / 6) + 'px'">
<svg

View File

@ -11,7 +11,8 @@ import { StateService } from '../../services/state.service';
export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {
@Input() width: number = 300;
@Input() height: number = 60;
@Input() mode: 'mempool' | 'block';
@Input() mode: 'mempool' | 'mined';
@Input() index: number = 0;
mempoolBlocks: number = 3;
blockchainBlocks: number = 6;
@ -70,4 +71,12 @@ export class ClockchainComponent implements OnInit, OnChanges, OnDestroy {
this.ltrTransitionEnabled = true;
this.stateService.timeLtr.next(!this.timeLtr);
}
getMempoolUrl(index): string {
return `/clock/mempool/${index}`;
}
getMinedUrl(index): string {
return `/clock/block/${index}`;
}
}

View File

@ -8,7 +8,7 @@
[style.right]="mempoolBlockStyles[i].right"
></div>
<div @blockEntryTrigger [@.disabled]="i > 0 || !animateEntry" [attr.data-cy]="'mempool-block-' + i" class="bitcoin-block text-center mempool-block" [class.hide-block]="count && i >= count" id="mempool-block-{{ i }}" [ngStyle]="mempoolBlockStyles[i]" [class.blink-bg]="projectedBlock.blink">
<a draggable="false" [routerLink]="['/mempool-block/' | relativeUrl, i]"
<a draggable="false" [routerLink]="[getHref(i) | relativeUrl]"
class="blockLink" [ngClass]="{'disabled': (this.stateService.blockScrolling$ | async)}">&nbsp;</a>
<div class="block-body">
<ng-container *ngIf="!minimal">

View File

@ -28,6 +28,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
@Input() blockWidth: number = 125;
@Input() count: number = null;
@Input() spotlight: number = 0;
@Input() getHref?: (index) => string = (index) => `/mempool-block/${index}`;
specialBlocks = specialBlocks;
mempoolBlocks: MempoolBlock[] = [];

View File

@ -12,12 +12,13 @@
<form [formGroup]="radioGroupForm" class="formRadioGroup"
[class]="(stateService.env.MINING_DASHBOARD || stateService.env.LIGHTNING) ? 'mining' : 'no-menu'" (click)="saveGraphPreference()">
<div *ngIf="!isMobile()" class="btn-group btn-group-toggle">
<label class="btn btn-primary btn-sm mr-2">
<a [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
</a>
</label>
<div class="small-buttons">
<a class="btn btn-primary btn-sm mb-0" [routerLink]="['/clock/mempool/0' | relativeUrl]" style="color: white" id="btn-clock">
<fa-icon [icon]="['fas', 'clock']" [fixedWidth]="true" i18n-title="master-page.clockview" title="Clock view"></fa-icon>
</a>
<a *ngIf="!isMobile()" class="btn btn-primary btn-sm mb-0" [routerLink]="['/tv' | relativeUrl]" style="color: white" id="btn-tv">
<fa-icon [icon]="['fas', 'tv']" [fixedWidth]="true" i18n-title="master-page.tvview" title="TV view"></fa-icon>
</a>
</div>
<div class="btn-group btn-group-toggle" name="radioBasic">
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('dateSpan').value === '2h'">

View File

@ -150,17 +150,30 @@
margin: 0px 0px;
}
.btn {
width: 49.25%;
width: 50%;
flex-grow: 1;
flex-shrink: 1;
margin: 1px 0;
margin-right: 0.5rem;
@media (min-width: 830px) {
width: auto;
}
&:first-child {
margin-right: 0;
@media (min-width: 830px) {
margin-right: 0.5rem;
}
}
}
.dropdown {
width: 49.25%;
display: flex;
width: 50%;
flex-grow: 1;
flex-shrink: 1;
@media (min-width: 830px) {
width: auto;
margin: 0px 5px;
margin-left: 0.5rem;
}
}
#dropdownFees {

View File

@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
import { NgbCollapseModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, faChartArea, faCogs, faCubes, faHammer, faDatabase, faExchangeAlt, faInfoCircle,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faLink, faList, faSearch, faCaretUp, faCaretDown, faTachometerAlt, faThList, faTint, faTv, faClock, faAngleDoubleDown, faSortUp, faAngleDoubleUp, faChevronDown,
faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode, faArrowRightArrowLeft, faArrowsRotate, faCircleLeft } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MasterPageComponent } from '../components/master-page/master-page.component';
@ -95,8 +95,6 @@ import { MempoolBlockOverviewComponent } from '../components/mempool-block-overv
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
import { ClockFaceComponent } from '../components/clock-face/clock-face.component';
import { ClockComponent } from '../components/clock/clock.component';
import { ClockMinedComponent } from '../components/clock/clock-mined.component';
import { ClockMempoolComponent } from '../components/clock/clock-mempool.component';
@NgModule({
declarations: [
@ -185,8 +183,6 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
MempoolBlockOverviewComponent,
ClockchainComponent,
ClockComponent,
ClockMinedComponent,
ClockMempoolComponent,
ClockFaceComponent,
],
imports: [
@ -300,8 +296,6 @@ import { ClockMempoolComponent } from '../components/clock/clock-mempool.compone
MempoolBlockOverviewComponent,
ClockchainComponent,
ClockComponent,
ClockMinedComponent,
ClockMempoolComponent,
ClockFaceComponent,
]
})
@ -310,6 +304,7 @@ export class SharedModule {
library.addIcons(faInfoCircle);
library.addIcons(faChartArea);
library.addIcons(faTv);
library.addIcons(faClock);
library.addIcons(faTachometerAlt);
library.addIcons(faCubes);
library.addIcons(faHammer);