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
4 changes: 3 additions & 1 deletion frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
@if (showLandingPage$ | async) {
<mina-web-node-landing-page (goToNode)="goToWebNode()"
(stopRequests)="clearNodeUpdateSubscription()"></mina-web-node-landing-page>
} @else {
} @else if (showLoadingWebNodePage$ | async) {
<router-outlet></router-outlet>
} @else if (loaded) {
<mat-sidenav-container [hasBackdrop]="false"
class="w-100 h-100"
*ngIf="menu$ | async as menu">
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { AppSelectors } from '@app/app.state';
import { AppActions } from '@app/app.actions';
import { filter, map, Observable, Subscription, take, timer } from 'rxjs';
import { CONFIG, getFirstFeature } from '@shared/constants/config';
import { CONFIG } from '@shared/constants/config';
import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class';
import { Router } from '@angular/router';
import { Routes } from '@shared/enums/routes.enum';

@Component({
selector: 'app-root',
Expand All @@ -20,8 +21,10 @@ export class AppComponent extends StoreDispatcher implements OnInit {

protected readonly menu$: Observable<AppMenu> = this.select$(AppSelectors.menu);
protected readonly showLandingPage$: Observable<boolean> = this.select$(getMergedRoute).pipe(filter(Boolean), map((route: MergedRoute) => route.url === '/'));
protected readonly showLoadingWebNodePage$: Observable<boolean> = this.select$(getMergedRoute).pipe(filter(Boolean), map((route: MergedRoute) => route.url === `/${Routes.LOADING_WEB_NODE}`));
subMenusLength: number = 0;
hideToolbar: boolean = CONFIG.hideToolbar;
loaded: boolean;

private nodeUpdateSubscription: Subscription | null = null;

Expand All @@ -42,10 +45,18 @@ export class AppComponent extends StoreDispatcher implements OnInit {
take(1),
filter((route: MergedRoute) => route.url !== '/'),
);
this.select(
getMergedRoute,
() => {
this.loaded = true;
this.detect();
},
filter(Boolean),
);
}

goToWebNode(): void {
this.router.navigate([getFirstFeature()]);
this.router.navigate([Routes.LOADING_WEB_NODE]);
this.initAppFunctionalities();
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { APP_INITIALIZER, ErrorHandler, Injectable, LOCALE_ID, NgModule, Provider } from '@angular/core';
import { APP_INITIALIZER, ErrorHandler, Injectable, LOCALE_ID, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/app/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const SNARKS_TITLE: string = APP_TITLE + ' - Snarks';
export const BLOCK_PRODUCTION_TITLE: string = APP_TITLE + ' - Block Production';
export const MEMPOOL_TITLE: string = APP_TITLE + ' - Mempool';
export const BENCHMARKS_TITLE: string = APP_TITLE + ' - Benchmarks';
export const WEBNODE_TITLE: string = APP_TITLE + ' - Web Node';


function generateRoutes(): Routes {
Expand Down Expand Up @@ -64,6 +65,11 @@ function generateRoutes(): Routes {
loadChildren: () => import('./features/benchmarks/benchmarks.module').then(m => m.BenchmarksModule),
title: BENCHMARKS_TITLE,
},
{
path: 'loading-web-node',
loadChildren: () => import('./features/webnode/webnode.module').then(m => m.WebnodeModule),
title: WEBNODE_TITLE,
},
];
if (CONFIG.showWebNodeLandingPage) {
routes.push({
Expand All @@ -76,7 +82,7 @@ function generateRoutes(): Routes {
...routes,
{
path: '**',
redirectTo: getFirstFeature(),
redirectTo: CONFIG.showWebNodeLandingPage ? '' : getFirstFeature(),
pathMatch: 'full',
},
];
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/app/core/services/web-node.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export class WebNodeService {
private webNodeStartTime: number;
private sentryEvents: any = {};

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

constructor(private http: HttpClient) {
const basex = base('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
any(window)['bs58btc'] = {
Expand Down Expand Up @@ -45,13 +47,15 @@ export class WebNodeService {
switchMap((wasm: any) => from(wasm.default('assets/webnode/pkg/openmina_node_web_bg.wasm')).pipe(map(() => wasm))),
switchMap((wasm) => {
sendSentryEvent('WebNode Wasm loaded. Starting WebNode');
this.webnodeProgress$.next('Loaded');
return from(wasm.run(this.webNodeKeyPair.privateKey));
}),
tap((webnode: any) => {
sendSentryEvent('WebNode Started');
this.webNodeStartTime = Date.now();
(window as any)['webnode'] = webnode;
this.webnode$.next(webnode);
this.webnodeProgress$.next('Started');
}),
catchError((error) => {
sendSentryEvent('WebNode failed to start');
Expand Down Expand Up @@ -83,7 +87,6 @@ export class WebNodeService {
switchMap(handle => from(any(handle).state().peers())),
tap((peers) => {
if (!this.sentryEvents.sentNoPeersEvent && Date.now() - this.webNodeStartTime >= 5000 && peers.length === 0) {
console.log('WebNode has no peers after 5 seconds from startup.');
sendSentryEvent('WebNode has no peers after 5 seconds from startup.');
this.sentryEvents.sentNoPeersEvent = true;
}
Expand All @@ -96,6 +99,7 @@ export class WebNodeService {
const seconds = (Date.now() - this.webNodeStartTime) / 1000;
sendSentryEvent(`WebNode connected to its first peer after ${seconds}s`);
this.sentryEvents.firstPeerConnected = true;
this.webnodeProgress$.next('Connected');
}
}),
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<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="loading-webnode f-500">
With the Web Node, you can produce blocks directly through your browser
</div>

<div class="font-16 tertiary pt-10 mt-10">
@if (!loading[loading.length - 1].loaded) {
Setting up your in-browser Web Node...
} @else {
Web Node is ready
}
</div>
</div>

<div class="mt-16 border-rad-6 bg-container flex-column p-12 w-100">
@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>
</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"
[ngClass]="errors.length ? 'warn-primary' : 'aware-primary'">
<div>It appears there are some errors.</div>
</div>
<div class="flex-column border-rad-8 warn-secondary monospace break-word">
@for (error of errors; track $index) {
<div class="lh-sm">{{ error }}</div>
}
</div>
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@import 'openmina';

$green: #59bfb5;
$light-peach: #0d0d0d;
$peach: #acdea0;
$sand: #5bb3fb;
$white: #000000;

.font-16 {
font-size: 16px;
}

.data-wrapper {
max-width: 568px;

.header {
margin-bottom: 56px;

.loading-webnode {
font-size: 24px;
line-height: 32px;
}
}

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

.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%;
}
}

@keyframes fadeIn {
10% {
opacity: 1;
}
38% {
opacity: 0.1;
}
42% {
opacity: 0.1;
}
70% {
opacity: 1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ChangeDetectionStrategy, Component, NgZone, OnInit } from '@angular/core';
import { untilDestroyed } from '@ngneat/until-destroy';
import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class';
import { WebNodeService } from '@core/services/web-node.service';
import { GlobalErrorHandlerService } from '@openmina/shared';
import { NgClass, NgForOf, NgIf } from '@angular/common';
import { Router } from '@angular/router';
import { getFirstFeature } from '@shared/constants/config';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { switchMap, timer } from 'rxjs';

export interface WebNodeDemoLoadingStep {
name: string;
loaded: boolean;
attempt?: number;
step: number;
}


@Component({
selector: 'mina-web-node-demo-dashboard',
templateUrl: './web-node-demo-dashboard.component.html',
styleUrls: ['./web-node-demo-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'flex-column h-100 w-100 align-center' },
standalone: true,
imports: [
NgClass,
NgIf,
NgForOf,
],
animations: [
trigger('fadeIn', [
state('void', style({ opacity: 0 })),
state('*', style({ opacity: 1 })),
transition('void => *', [
animate('.6s ease-in'),
]),
]),
],
})
export class WebNodeDemoDashboardComponent extends StoreDispatcher implements OnInit {

readonly loading: WebNodeDemoLoadingStep[] = [
{ name: 'Setting up browser for Web Node', loaded: false, step: 1 },
{ name: 'Getting ready to produce blocks', loaded: false, step: 2 },
{ name: 'Connecting directly to Mina network', loaded: false, step: 3 },
];
ready: boolean = false;
errors: string[] = [];

constructor(private errorHandler: GlobalErrorHandlerService,
private webNodeService: WebNodeService,
private zone: NgZone,
private router: Router) { super(); }

ngOnInit(): void {
this.listenToErrorIssuing();
this.checkWebNodeProgress();
this.fetchPeersInformation();
}

private checkWebNodeProgress(): void {
this.webNodeService.webnodeProgress$.pipe(untilDestroyed(this)).subscribe((state: string) => {
if (state === 'Loaded') {
this.loading[0].loaded = true;
} else if (state === 'Started') {
this.loading[0].loaded = true;
this.loading[1].loaded = true;
} else if (state === 'Connected') {
this.loading.forEach((step: WebNodeDemoLoadingStep) => step.loaded = true);
}
this.ready = this.loading.every((step: WebNodeDemoLoadingStep) => step.loaded);
this.detect();
});
}

private fetchPeersInformation(): void {
timer(0, 1000).pipe(
switchMap(() => this.webNodeService.peers$),
untilDestroyed(this),
).subscribe();
}

private listenToErrorIssuing(): void {
this.errorHandler.errors$
.pipe(untilDestroyed(this))
.subscribe((error: string) => {
console.log(error);
});
}

goToDashboard(): void {
if (!this.ready) {
return;
}
this.router.navigate([getFirstFeature()]);
}
}
1 change: 1 addition & 0 deletions frontend/src/app/features/webnode/webnode.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mina-web-node-demo-dashboard></mina-web-node-demo-dashboard>
Empty file.
18 changes: 18 additions & 0 deletions frontend/src/app/features/webnode/webnode.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import {
WebNodeDemoDashboardComponent,
} from '@app/features/webnode/web-node-demo-dashboard/web-node-demo-dashboard.component';

@Component({
selector: 'mina-webnode',
standalone: true,
imports: [
WebNodeDemoDashboardComponent,
],
templateUrl: './webnode.component.html',
styleUrl: './webnode.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class WebnodeComponent {

}
14 changes: 14 additions & 0 deletions frontend/src/app/features/webnode/webnode.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { WebnodeRouting } from './webnode.routing';


@NgModule({
declarations: [],
imports: [
CommonModule,
WebnodeRouting,
],
})
export class WebnodeModule {}
16 changes: 16 additions & 0 deletions frontend/src/app/features/webnode/webnode.routing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { WebnodeComponent } from '@app/features/webnode/webnode.component';

const routes: Routes = [
{
path: '',
component: WebnodeComponent,
},
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class WebnodeRouting {}
Loading
Loading
You are viewing a condensed version of this merge commit. You can view the full changes here.