Add acceleration timeline

This commit is contained in:
natsoni 2024-07-04 16:54:35 +09:00
parent 815dcbd4ce
commit 6b481d5a07
No known key found for this signature in database
GPG Key ID: C65917583181743B
5 changed files with 320 additions and 0 deletions

View File

@ -0,0 +1,78 @@
<div class="acceleration-timeline box">
<div class="timeline-wrapper">
<div class="timeline">
<div class="intervals">
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
<app-time [time]="acceleratedAt - transactionTime"></app-time>
</div>
</div>
<div class="node-spacer"></div>
<div class="interval">
<div class="interval-time">
@if (eta) {
~<app-time kind="plain" [time]="eta?.wait / 1000"></app-time>
} @else if (tx.status.block_time) {
<app-time kind="plain" [time]="tx.status.block_time - acceleratedAt"></app-time>
}
</div>
</div>
<div class="node-spacer"></div>
</div>
</div>
<div class="nodes">
<div class="node" [id]="'first-seen'">
<div class="seen-to-acc right" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.sent-selected]="!tx.status.confirmed && !tx.acceleration">
<div class="shape"></div>
</a>
<div class="status"><span class="badge badge-primary">Sent</span></div>
<div class="time">
<app-time *ngIf="transactionTime > 0" kind="since" [time]="transactionTime"></app-time>
</div>
</div>
<div class="interval-spacer">
<div class="seen-to-acc" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
</div>
<div class="node" [id]="'accelerated'">
<div class="seen-to-acc left" [class.loading]="!tx.acceleration && !tx.status.confirmed"></div>
<div class="acc-to-confirmed right" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.accelerated-selected]="tx.acceleration && !tx.status.confirmed">
<div class="shape"></div>
</a>
<div class="status" [style]="!tx.acceleration && !tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-accelerated">Accelerated</span></div>
<div class="time">
<app-time *ngIf="acceleratedAt" kind="since" [time]="acceleratedAt"></app-time>
</div>
</div>
<div class="interval-spacer">
<div class="acc-to-confirmed" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
</div>
<div class="node" [id]="'confirmed'" [class.mined]="tx.status.confirmed">
<div class="acc-to-confirmed left" [class.loading]="tx.acceleration && !tx.status.confirmed"></div>
<a class="shape-border" [class.mined-selected]="tx.status.confirmed">
<div class="shape"></div>
</a>
<div class="status" [style]="!tx.status.confirmed ? 'opacity: 0.5' : ''"><span class="badge badge-success">Mined</span></div>
<div class="time">
@if (tx.status.block_time) {
<app-time kind="since" [time]="tx.status.block_time"></app-time>
} @else if (eta) {
<app-time kind="until" [time]="eta?.time"></app-time>
}
</div>
</div>
</div>
</div>
<ng-template #nodeSpacer>
<div class="node-spacer"></div>
</ng-template>
<ng-template #intervalSpacer>
<div class="interval-spacer"></div>
</ng-template>
</div>

View File

@ -0,0 +1,197 @@
.acceleration-timeline {
position: relative;
width: 100%;
padding: 1em 0;
&::after, &::before {
content: '';
display: block;
position: absolute;
top: 0;
bottom: 0;
width: 2em;
z-index: 2;
}
&::before {
left: 0;
background: linear-gradient(to right, var(--box-bg), var(--box-bg), transparent);
}
&::after {
right: 0;
background: linear-gradient(to left, var(--box-bg), var(--box-bg), transparent);
}
.timeline-wrapper {
position: relative;
width: calc(100% - 2em);
margin: auto;
overflow-x: auto;
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.intervals, .nodes {
min-width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
text-align: center;
.node, .node-spacer {
width: 6em;
min-width: 6em;
flex-grow: 1;
}
.interval, .interval-spacer {
width: 8em;
min-width: 5em;
max-width: 8em;
height: 32px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-end;
}
.interval {
overflow: visible;
}
.interval-time {
font-size: 12px;
line-height: 16px;
white-space: nowrap;
}
}
.node, .interval-spacer {
position: relative;
.seen-to-acc {
position: absolute;
height: 10px;
left: -5px;
right: -5px;
top: 0;
transform: translateY(-50%);
background: var(--primary);
border-radius: 5px;
&.loading {
animation: standardPulse 1s infinite;
}
&.left {
right: 50%;
}
&.right {
left: 50%;
}
}
.acc-to-confirmed {
position: absolute;
height: 10px;
left: -5px;
right: -5px;
top: 0;
transform: translateY(-50%);
background: var(--tertiary);
border-radius: 5px;
&.loading {
animation: acceleratePulse 1s infinite;
}
&.left {
right: 50%;
}
&.right {
left: 50%;
}
}
}
.nodes {
position: relative;
margin-top: 1em;
.node {
.shape-border {
display: block;
margin: auto;
height: calc(1em + 8px);
width: calc(1em + 8px);
margin-bottom: -8px;
transform: translateY(-50%);
border-radius: 50%;
padding: 2px;
background: transparent;
transition: background-color 300ms, padding 300ms;
.shape {
width: 100%;
height: 100%;
border-radius: 50%;
background: white;
transition: background-color 300ms, border 300ms;
}
&.sent-selected {
.shape {
background: var(--primary);
}
}
&.accelerated-selected {
.shape {
background: var(--tertiary);
}
}
&.mined-selected {
.shape {
background: var(--success);
}
}
}
.status {
margin-top: -64px;
.badge.badge-accelerated {
background-color: var(--tertiary);
color: white;
}
}
.time {
margin-top: 33px;
font-size: 12px;
line-height: 16px;
white-space: nowrap;
}
}
}
}
@keyframes acceleratePulse {
0% { background-color: var(--tertiary) }
50% { background-color: var(--mainnet-alt) }
100% { background-color: var(--tertiary) }
}
@keyframes standardPulse {
0% { background-color: var(--primary) }
50% { background-color: var(--secondary) }
100% { background-color: var(--primary) }
}

View File

@ -0,0 +1,33 @@
import { Component, Input, OnInit, OnChanges, Inject, LOCALE_ID } from '@angular/core';
import { ETA } from '../../services/eta.service';
import { Transaction } from '../../interfaces/electrs.interface';
@Component({
selector: 'app-acceleration-timeline',
templateUrl: './acceleration-timeline.component.html',
styleUrls: ['./acceleration-timeline.component.scss'],
})
export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number;
@Input() tx: Transaction;
@Input() eta: ETA;
acceleratedAt: number;
dir: 'rtl' | 'ltr' = 'ltr';
constructor(
@Inject(LOCALE_ID) private locale: string,
) {
if (this.locale.startsWith('ar') || this.locale.startsWith('fa') || this.locale.startsWith('he')) {
this.dir = 'rtl';
}
}
ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000;
}
ngOnChanges(changes): void {
}
}

View File

@ -152,6 +152,15 @@
<br> <br>
<ng-container *ngIf="transactionTime && (tx.acceleration || isAcceleration)">
<div class="title float-left">
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</div>
<div class="clearfix"></div>
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [eta]="(ETA$ | async)"></app-acceleration-timeline>
<br>
</ng-container>
<ng-container *ngIf="rbfInfo"> <ng-container *ngIf="rbfInfo">
<div class="title float-left"> <div class="title float-left">
<h2 id="rbf" i18n="transaction.rbf-history|RBF History">RBF History</h2> <h2 id="rbf" i18n="transaction.rbf-history|RBF History">RBF History</h2>

View File

@ -66,6 +66,7 @@ import { DifficultyMiningComponent } from '../components/difficulty-mining/diffi
import { BalanceWidgetComponent } from '../components/balance-widget/balance-widget.component'; import { BalanceWidgetComponent } from '../components/balance-widget/balance-widget.component';
import { AddressTransactionsWidgetComponent } from '../components/address-transactions-widget/address-transactions-widget.component'; import { AddressTransactionsWidgetComponent } from '../components/address-transactions-widget/address-transactions-widget.component';
import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component'; import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component';
import { AccelerationTimelineComponent } from '../components/acceleration-timeline/acceleration-timeline.component';
import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component'; import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component';
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component'; import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component';
@ -177,6 +178,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BalanceWidgetComponent, BalanceWidgetComponent,
AddressTransactionsWidgetComponent, AddressTransactionsWidgetComponent,
RbfTimelineComponent, RbfTimelineComponent,
AccelerationTimelineComponent,
RbfTimelineTooltipComponent, RbfTimelineTooltipComponent,
PushTransactionComponent, PushTransactionComponent,
TestTransactionsComponent, TestTransactionsComponent,
@ -316,6 +318,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
BalanceWidgetComponent, BalanceWidgetComponent,
AddressTransactionsWidgetComponent, AddressTransactionsWidgetComponent,
RbfTimelineComponent, RbfTimelineComponent,
AccelerationTimelineComponent,
RbfTimelineTooltipComponent, RbfTimelineTooltipComponent,
PushTransactionComponent, PushTransactionComponent,
TestTransactionsComponent, TestTransactionsComponent,