Skip to content

Commit 3f18c5e

Browse files
authored
Frontend - Loading Percentage for web node (#872)
1 parent 1bb5bd4 commit 3f18c5e

File tree

9 files changed

+428
-160
lines changed

9 files changed

+428
-160
lines changed

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"start": "npm install && ng serve --configuration local --open",
77
"start:dev": "ng serve --configuration development",
88
"build": "ng build",
9-
"build:prod": "npm run prebuild && ng build --configuration production",
9+
"build:prod": "ng build --configuration production",
1010
"tests": "npx cypress open --config baseUrl=http://localhost:4200",
1111
"tests:headless": "npx cypress run --headless --config baseUrl=http://localhost:4200",
1212
"docker": "npm run build:prod && docker buildx build --platform linux/amd64 -t openmina/frontend:latest . && docker push openmina/frontend:latest",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { BehaviorSubject } from 'rxjs';
2+
3+
class AssetMonitor {
4+
readonly downloads: Map<string, any> = new Map();
5+
readonly progress$: BehaviorSubject<any>;
6+
7+
constructor(progress$: BehaviorSubject<any>) {
8+
this.progress$ = progress$;
9+
this.setupInterceptor();
10+
}
11+
12+
private setupInterceptor(): void {
13+
const originalFetch = window.fetch;
14+
const self = this;
15+
16+
window.fetch = async function (resource, options) {
17+
// Only intercept asset requests (you can modify these extensions as needed)
18+
const assetExtensions = ['.wasm'];
19+
const isAsset = assetExtensions.some(ext =>
20+
resource.toString().toLowerCase().endsWith(ext),
21+
);
22+
23+
if (!isAsset) {
24+
return originalFetch(resource, options);
25+
}
26+
27+
const startTime = performance.now();
28+
const downloadInfo = {
29+
url: resource.toString(),
30+
startTime,
31+
progress: 0,
32+
totalSize: 0,
33+
status: 'pending',
34+
endTime: 0,
35+
duration: 0,
36+
};
37+
38+
self.downloads.set(resource.toString(), downloadInfo);
39+
self.emitProgress(downloadInfo);
40+
41+
try {
42+
const response = await originalFetch(resource, options);
43+
const reader = response.clone().body.getReader();
44+
const contentLength = +response.headers.get('Content-Length');
45+
downloadInfo.totalSize = contentLength;
46+
let receivedLength = 0;
47+
48+
while (true) {
49+
try {
50+
const { done, value } = await reader.read();
51+
52+
if (done) {
53+
break;
54+
}
55+
56+
receivedLength += value.length;
57+
downloadInfo.progress = (receivedLength / contentLength) * 100;
58+
self.emitProgress(downloadInfo);
59+
} catch (error) {
60+
downloadInfo.status = 'error';
61+
self.emitProgress(downloadInfo);
62+
throw error;
63+
}
64+
}
65+
66+
downloadInfo.status = 'complete';
67+
downloadInfo.endTime = performance.now();
68+
downloadInfo.duration = downloadInfo.endTime - downloadInfo.startTime;
69+
self.emitProgress(downloadInfo);
70+
return await response;
71+
} catch (error_1) {
72+
downloadInfo.status = 'error';
73+
self.emitProgress(downloadInfo);
74+
throw error_1;
75+
}
76+
};
77+
}
78+
79+
private emitProgress(downloadInfo: any): void {
80+
this.progress$.next({
81+
url: downloadInfo.url,
82+
progress: downloadInfo.progress.toFixed(2),
83+
totalSize: downloadInfo.totalSize,
84+
status: downloadInfo.status,
85+
duration: downloadInfo.duration,
86+
startTime: downloadInfo.startTime,
87+
endTime: downloadInfo.endTime,
88+
downloaded: downloadInfo.progress * downloadInfo.totalSize / 100,
89+
});
90+
}
91+
}
92+
93+
export class FileProgressHelper {
94+
static progress$: BehaviorSubject<any> = new BehaviorSubject<any>(null);
95+
96+
static initDownloadProgress(): void {
97+
new AssetMonitor(this.progress$);
98+
}
99+
}

frontend/src/app/core/services/web-node.service.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { Injectable } from '@angular/core';
1+
import { Inject, Injectable } from '@angular/core';
22
import { BehaviorSubject, catchError, filter, from, fromEvent, map, merge, Observable, of, switchMap, tap } from 'rxjs';
33
import base from 'base-x';
44
import { any } from '@openmina/shared';
55
import { HttpClient } from '@angular/common/http';
66
import { sendSentryEvent } from '@shared/helpers/webnode.helper';
77
import { DashboardPeerStatus } from '@shared/types/dashboard/dashboard.peer';
8+
import { DOCUMENT } from '@angular/common';
9+
import { FileProgressHelper } from '@core/helpers/file-progress.helper';
810

911
@Injectable({
1012
providedIn: 'root',
@@ -18,15 +20,42 @@ export class WebNodeService {
1820

1921
readonly webnodeProgress$: BehaviorSubject<string> = new BehaviorSubject<string>('');
2022

21-
constructor(private http: HttpClient) {
23+
constructor(private http: HttpClient,
24+
@Inject(DOCUMENT) private document: Document) {
25+
FileProgressHelper.initDownloadProgress();
2226
const basex = base('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
2327
any(window)['bs58btc'] = {
2428
encode: (buffer: Uint8Array | number[]) => 'z' + basex.encode(buffer),
2529
decode: (string: string) => basex.decode(string.substring(1)),
2630
};
2731
}
2832

33+
private loadWebnodeJs(): void {
34+
if (this.document.querySelector('[data-webnode]')) {
35+
return;
36+
}
37+
38+
const script = this.document.createElement('script');
39+
script.type = 'module';
40+
script.setAttribute('data-webnode', 'true');
41+
script.textContent = `
42+
import('./assets/webnode/pkg/openmina_node_web.js')
43+
.then(v => {
44+
window.webnode = v;
45+
window.dispatchEvent(new CustomEvent('webNodeLoaded'));
46+
})
47+
.catch(er => {
48+
if (window.env?.configs.some(c => c.isWebNode)) {
49+
console.error('Failed to load Web Node:', er);
50+
}
51+
});
52+
`;
53+
54+
this.document.body.appendChild(script);
55+
}
56+
2957
loadWasm$(): Observable<void> {
58+
this.loadWebnodeJs();
3059
sendSentryEvent('Loading WebNode JS');
3160
return merge(
3261
of(any(window).webnode).pipe(filter(Boolean)),
Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,51 @@
1-
<div class="data-wrapper h-100 fx-col-full-cent">
2-
<div class="header flex-column align-center mb-16 pb-12 text-center">
1+
<div class="logo-header w-100 border-bottom fx-row-full-cent">
2+
<img ngSrc="assets/images/logo/logo-text.svg" height="40" width="136"/>
3+
</div>
4+
<div class="data-wrapper fx-col-full-cent">
5+
<div class="header flex-column align-center mb-16 pb-12 pl-10 pr-10 text-center">
36
<div class="loading-webnode f-500">
4-
With the Web Node, you can produce blocks directly through your browser
7+
Produce blocks,
8+
<span>&nbsp;right in your browser</span>
59
</div>
610

7-
<div class="font-16 tertiary pt-10 mt-10">
11+
<div class="font-16 tertiary mt-16 font-16 f-300">
812
@if (!loading[loading.length - 1].loaded) {
9-
Setting up your in-browser Web Node...
13+
@if (loading[0].status === WebNodeStepStatus.LOADING) {
14+
Downloading...
15+
} @else {
16+
~7 seconds left
17+
}
1018
} @else {
1119
Web Node is ready
1220
}
1321
</div>
1422
</div>
1523

16-
<div class="mt-16 border-rad-6 bg-container flex-column p-12 w-100">
24+
<div #progress class="progress w-100"></div>
25+
26+
<div class="mt-16 flex-column w-100 pl-10 pr-10">
1727
@for (item of loading; track $index) {
18-
<div class="flex-row flex-between align-center h-xl f-500 font-16">
19-
<div [ngClass]="item.loaded ? 'tertiary' : 'primary'">{{ item.name }}</div>
20-
<div class="flex-row align-center flex-center w-lg h-md border-rad-6"
21-
[ngClass]="'bg-' + (item.loaded ? 'success' : (!item.loaded && (loading[$index - 1]?.loaded || $index === 0)) ? 'aware' : '') + '-container'">
22-
<span class="mina-icon icon-200"
23-
[ngClass]="(item.loaded ? 'success-' : (!item.loaded && (loading[$index - 1]?.loaded || $index === 0)) ? 'aware-' : '') + 'primary'">
24-
{{ item.loaded ? 'task_alt' : 'more_horiz' }}
25-
</span>
28+
<div class="fx-row-vert-cent h-xl font-16">
29+
@if (item.status === WebNodeStepStatus.LOADING) {
30+
<mina-loading-spinner [size]="20" [borderWidth]="2"></mina-loading-spinner>
31+
} @else {
32+
<span [ngClass]="item.status === WebNodeStepStatus.DONE ? 'success-primary' : 'tertiary'"
33+
class="circle-check mina-icon icon-200">check_circle</span>
34+
}
35+
<div [ngClass]="item.status === WebNodeStepStatus.PENDING ? 'tertiary' : 'primary'"
36+
class="ml-8 f-400">{{ item.name }}
2637
</div>
38+
@if (item.data) {
39+
@if (item.data.total) {
40+
<span class="tertiary ml-5">{{ item.data.downloaded }} of {{ item.data.total }} MB</span>
41+
} @else {
42+
<span class="tertiary ml-5">{{ item.data.est }}</span>
43+
}
44+
}
2745
</div>
2846
}
2947
</div>
3048

31-
<div class="p-relative">
32-
@if (ready) {
33-
<button
34-
[@fadeIn]="ready"
35-
class="p-absolute launch f-500 border-rad-6 flex-row align-center flex-center cta-primary"
36-
(click)="goToDashboard()">
37-
Launch Block Producer
38-
</button>
39-
}
40-
</div>
41-
4249
@if (!loading[loading.length - 1].loaded && errors.length) {
4350
<div class="flex-column w-100 bg-surface p-12 mt-10 border-rad-6">
4451
<div class="font-16 flex-row flex-column-md flex-between align-center flex-start-md"
@@ -53,3 +60,16 @@
5360
</div>
5461
}
5562
</div>
63+
<div class="footer border-top fx-row-vert-cent flex-between w-100 pl-10 pr-10">
64+
<div class="fx-row-vert-cent">
65+
<span class="mina-icon icon-300 selected-primary mr-5">lightbulb</span>
66+
<div class="selected-primary f-big f-500">
67+
You can run the Web Node from your phone
68+
</div>
69+
</div>
70+
<button class="border-rad-8 fx-row-full-cent font-16 ml-10"
71+
[class.disabled]="!loading[loading.length - 1].loaded"
72+
(click)="goToDashboard()">
73+
<span>Continue</span>
74+
</button>
75+
</div>

frontend/src/app/features/webnode/web-node-demo-dashboard/web-node-demo-dashboard.component.scss

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ $white: #000000;
1010
font-size: 16px;
1111
}
1212

13+
.logo-header {
14+
height: 56px;
15+
}
16+
1317
.data-wrapper {
18+
height: calc(100% - 56px - 72px);
1419
max-width: 568px;
1520

1621
.header {
@@ -22,25 +27,41 @@ $white: #000000;
2227
}
2328
}
2429

30+
mina-loading-spinner {
31+
margin: 0 2px;
32+
}
33+
34+
.mina-icon.circle-check {
35+
font-variation-settings: 'FILL' 1, 'wght' 300 !important;
36+
}
37+
2538
.mina-icon.aware-primary {
2639
animation: fadeIn 1500ms ease-in-out infinite;
2740
}
41+
42+
.progress {
43+
height: 380px;
44+
margin: 64px 0;
45+
}
2846
}
2947

30-
.launch {
31-
height: 56px !important;
32-
font-size: 20px;
33-
width: 341px;
34-
background: linear-gradient(to right, $green 0%, $sand 50%, $peach 100%);
35-
background-size: 500%;
36-
margin-top: 64px;
37-
transition: background-position 1s ease;
38-
background-position: 0 50%;
39-
left: 50%;
40-
transform: translateX(-50%);
41-
42-
&:hover {
43-
background-position: 100% 50%;
48+
.footer {
49+
height: 72px;
50+
max-width: 568px;
51+
52+
button {
53+
width: 158px;
54+
height: 48px !important;
55+
background-color: $cta-container;
56+
57+
&.disabled {
58+
opacity: 0.25;
59+
pointer-events: none;
60+
}
61+
62+
&:hover:not(.disabled) {
63+
background-color: $selected-primary;
64+
}
4465
}
4566
}
4667

@@ -58,3 +79,12 @@ $white: #000000;
5879
opacity: 1;
5980
}
6081
}
82+
83+
@media (max-width: 768px) {
84+
.font-16 {
85+
font-size: 15px;
86+
}
87+
.loading-webnode span {
88+
display: block;
89+
}
90+
}

0 commit comments

Comments
 (0)