Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"start": "npm install && ng serve --configuration local --open",
"start:dev": "ng serve --configuration development",
"build": "ng build",
"build:prod": "npm run prebuild && ng build --configuration production",
"build:prod": "ng build --configuration production",
"tests": "npx cypress open --config baseUrl=http://localhost:4200",
"tests:headless": "npx cypress run --headless --config baseUrl=http://localhost:4200",
"docker": "npm run build:prod && docker buildx build --platform linux/amd64 -t openmina/frontend:latest . && docker push openmina/frontend:latest",
Expand Down
99 changes: 99 additions & 0 deletions frontend/src/app/core/helpers/file-progress.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { BehaviorSubject } from 'rxjs';

class AssetMonitor {
readonly downloads: Map<string, any> = new Map();
readonly progress$: BehaviorSubject<any>;

constructor(progress$: BehaviorSubject<any>) {
this.progress$ = progress$;
this.setupInterceptor();
}

private setupInterceptor(): void {
const originalFetch = window.fetch;
const self = this;

window.fetch = async function (resource, options) {
// Only intercept asset requests (you can modify these extensions as needed)
const assetExtensions = ['.wasm'];
const isAsset = assetExtensions.some(ext =>
resource.toString().toLowerCase().endsWith(ext),
);

if (!isAsset) {
return originalFetch(resource, options);
}

const startTime = performance.now();
const downloadInfo = {
url: resource.toString(),
startTime,
progress: 0,
totalSize: 0,
status: 'pending',
endTime: 0,
duration: 0,
};

self.downloads.set(resource.toString(), downloadInfo);
self.emitProgress(downloadInfo);

try {
const response = await originalFetch(resource, options);
const reader = response.clone().body.getReader();
const contentLength = +response.headers.get('Content-Length');
downloadInfo.totalSize = contentLength;
let receivedLength = 0;

while (true) {
try {
const { done, value } = await reader.read();

if (done) {
break;
}

receivedLength += value.length;
downloadInfo.progress = (receivedLength / contentLength) * 100;
self.emitProgress(downloadInfo);
} catch (error) {
downloadInfo.status = 'error';
self.emitProgress(downloadInfo);
throw error;
}
}

downloadInfo.status = 'complete';
downloadInfo.endTime = performance.now();
downloadInfo.duration = downloadInfo.endTime - downloadInfo.startTime;
self.emitProgress(downloadInfo);
return await response;
} catch (error_1) {
downloadInfo.status = 'error';
self.emitProgress(downloadInfo);
throw error_1;
}
};
}

private emitProgress(downloadInfo: any): void {
this.progress$.next({
url: downloadInfo.url,
progress: downloadInfo.progress.toFixed(2),
totalSize: downloadInfo.totalSize,
status: downloadInfo.status,
duration: downloadInfo.duration,
startTime: downloadInfo.startTime,
endTime: downloadInfo.endTime,
downloaded: downloadInfo.progress * downloadInfo.totalSize / 100,
});
}
}

export class FileProgressHelper {
static progress$: BehaviorSubject<any> = new BehaviorSubject<any>(null);

static initDownloadProgress(): void {
new AssetMonitor(this.progress$);
}
}
33 changes: 31 additions & 2 deletions frontend/src/app/core/services/web-node.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject, catchError, filter, from, fromEvent, map, merge, Observable, of, switchMap, tap } from 'rxjs';
import base from 'base-x';
import { any } from '@openmina/shared';
import { HttpClient } from '@angular/common/http';
import { sendSentryEvent } from '@shared/helpers/webnode.helper';
import { DashboardPeerStatus } from '@shared/types/dashboard/dashboard.peer';
import { DOCUMENT } from '@angular/common';
import { FileProgressHelper } from '@core/helpers/file-progress.helper';

@Injectable({
providedIn: 'root',
Expand All @@ -18,15 +20,42 @@ export class WebNodeService {

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

constructor(private http: HttpClient) {
constructor(private http: HttpClient,
@Inject(DOCUMENT) private document: Document) {
FileProgressHelper.initDownloadProgress();
const basex = base('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
any(window)['bs58btc'] = {
encode: (buffer: Uint8Array | number[]) => 'z' + basex.encode(buffer),
decode: (string: string) => basex.decode(string.substring(1)),
};
}

private loadWebnodeJs(): void {
if (this.document.querySelector('[data-webnode]')) {
return;
}

const script = this.document.createElement('script');
script.type = 'module';
script.setAttribute('data-webnode', 'true');
script.textContent = `
import('./assets/webnode/pkg/openmina_node_web.js')
.then(v => {
window.webnode = v;
window.dispatchEvent(new CustomEvent('webNodeLoaded'));
})
.catch(er => {
if (window.env?.configs.some(c => c.isWebNode)) {
console.error('Failed to load Web Node:', er);
}
});
`;

this.document.body.appendChild(script);
}

loadWasm$(): Observable<void> {
this.loadWebnodeJs();
sendSentryEvent('Loading WebNode JS');
return merge(
of(any(window).webnode).pipe(filter(Boolean)),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,51 @@
<div class="data-wrapper h-100 fx-col-full-cent">
<div class="header flex-column align-center mb-16 pb-12 text-center">
<div class="logo-header w-100 border-bottom fx-row-full-cent">
<img ngSrc="assets/images/logo/logo-text.svg" height="40" width="136"/>
</div>
<div class="data-wrapper fx-col-full-cent">
<div class="header flex-column align-center mb-16 pb-12 pl-10 pr-10 text-center">
<div class="loading-webnode f-500">
With the Web Node, you can produce blocks directly through your browser
Produce blocks,
<span>&nbsp;right in your browser</span>
</div>

<div class="font-16 tertiary pt-10 mt-10">
<div class="font-16 tertiary mt-16 font-16 f-300">
@if (!loading[loading.length - 1].loaded) {
Setting up your in-browser Web Node...
@if (loading[0].status === WebNodeStepStatus.LOADING) {
Downloading...
} @else {
~7 seconds left
}
} @else {
Web Node is ready
}
</div>
</div>

<div class="mt-16 border-rad-6 bg-container flex-column p-12 w-100">
<div #progress class="progress w-100"></div>

<div class="mt-16 flex-column w-100 pl-10 pr-10">
@for (item of loading; track $index) {
<div class="flex-row flex-between align-center h-xl f-500 font-16">
<div [ngClass]="item.loaded ? 'tertiary' : 'primary'">{{ item.name }}</div>
<div class="flex-row align-center flex-center w-lg h-md border-rad-6"
[ngClass]="'bg-' + (item.loaded ? 'success' : (!item.loaded && (loading[$index - 1]?.loaded || $index === 0)) ? 'aware' : '') + '-container'">
<span class="mina-icon icon-200"
[ngClass]="(item.loaded ? 'success-' : (!item.loaded && (loading[$index - 1]?.loaded || $index === 0)) ? 'aware-' : '') + 'primary'">
{{ item.loaded ? 'task_alt' : 'more_horiz' }}
</span>
<div class="fx-row-vert-cent h-xl font-16">
@if (item.status === WebNodeStepStatus.LOADING) {
<mina-loading-spinner [size]="20" [borderWidth]="2"></mina-loading-spinner>
} @else {
<span [ngClass]="item.status === WebNodeStepStatus.DONE ? 'success-primary' : 'tertiary'"
class="circle-check mina-icon icon-200">check_circle</span>
}
<div [ngClass]="item.status === WebNodeStepStatus.PENDING ? 'tertiary' : 'primary'"
class="ml-8 f-400">{{ item.name }}
</div>
@if (item.data) {
@if (item.data.total) {
<span class="tertiary ml-5">{{ item.data.downloaded }} of {{ item.data.total }} MB</span>
} @else {
<span class="tertiary ml-5">{{ item.data.est }}</span>
}
}
</div>
}
</div>

<div class="p-relative">
@if (ready) {
<button
[@fadeIn]="ready"
class="p-absolute launch f-500 border-rad-6 flex-row align-center flex-center cta-primary"
(click)="goToDashboard()">
Launch Block Producer
</button>
}
</div>

@if (!loading[loading.length - 1].loaded && errors.length) {
<div class="flex-column w-100 bg-surface p-12 mt-10 border-rad-6">
<div class="font-16 flex-row flex-column-md flex-between align-center flex-start-md"
Expand All @@ -53,3 +60,16 @@
</div>
}
</div>
<div class="footer border-top fx-row-vert-cent flex-between w-100 pl-10 pr-10">
<div class="fx-row-vert-cent">
<span class="mina-icon icon-300 selected-primary mr-5">lightbulb</span>
<div class="selected-primary f-big f-500">
You can run the Web Node from your phone
</div>
</div>
<button class="border-rad-8 fx-row-full-cent font-16 ml-10"
[class.disabled]="!loading[loading.length - 1].loaded"
(click)="goToDashboard()">
<span>Continue</span>
</button>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ $white: #000000;
font-size: 16px;
}

.logo-header {
height: 56px;
}

.data-wrapper {
height: calc(100% - 56px - 72px);
max-width: 568px;

.header {
Expand All @@ -22,25 +27,41 @@ $white: #000000;
}
}

mina-loading-spinner {
margin: 0 2px;
}

.mina-icon.circle-check {
font-variation-settings: 'FILL' 1, 'wght' 300 !important;
}

.mina-icon.aware-primary {
animation: fadeIn 1500ms ease-in-out infinite;
}

.progress {
height: 380px;
margin: 64px 0;
}
}

.launch {
height: 56px !important;
font-size: 20px;
width: 341px;
background: linear-gradient(to right, $green 0%, $sand 50%, $peach 100%);
background-size: 500%;
margin-top: 64px;
transition: background-position 1s ease;
background-position: 0 50%;
left: 50%;
transform: translateX(-50%);

&:hover {
background-position: 100% 50%;
.footer {
height: 72px;
max-width: 568px;

button {
width: 158px;
height: 48px !important;
background-color: $cta-container;

&.disabled {
opacity: 0.25;
pointer-events: none;
}

&:hover:not(.disabled) {
background-color: $selected-primary;
}
}
}

Expand All @@ -58,3 +79,12 @@ $white: #000000;
opacity: 1;
}
}

@media (max-width: 768px) {
.font-16 {
font-size: 15px;
}
.loading-webnode span {
display: block;
}
}
Loading
Loading