Skip to content

Commit f63efa5

Browse files
authored
Frontend - Web node loading page v1 (#858)
Frontend - Web node loading page v1 (#858)
1 parent 2593874 commit f63efa5

16 files changed

+310
-23
lines changed

frontend/src/app/app.component.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
@if (showLandingPage$ | async) {
22
<mina-web-node-landing-page (goToNode)="goToWebNode()"
33
(stopRequests)="clearNodeUpdateSubscription()"></mina-web-node-landing-page>
4-
} @else {
4+
} @else if (showLoadingWebNodePage$ | async) {
5+
<router-outlet></router-outlet>
6+
} @else if (loaded) {
57
<mat-sidenav-container [hasBackdrop]="false"
68
class="w-100 h-100"
79
*ngIf="menu$ | async as menu">

frontend/src/app/app.component.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
55
import { AppSelectors } from '@app/app.state';
66
import { AppActions } from '@app/app.actions';
77
import { filter, map, Observable, Subscription, take, timer } from 'rxjs';
8-
import { CONFIG, getFirstFeature } from '@shared/constants/config';
8+
import { CONFIG } from '@shared/constants/config';
99
import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class';
1010
import { Router } from '@angular/router';
11+
import { Routes } from '@shared/enums/routes.enum';
1112

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

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

2629
private nodeUpdateSubscription: Subscription | null = null;
2730

@@ -42,10 +45,18 @@ export class AppComponent extends StoreDispatcher implements OnInit {
4245
take(1),
4346
filter((route: MergedRoute) => route.url !== '/'),
4447
);
48+
this.select(
49+
getMergedRoute,
50+
() => {
51+
this.loaded = true;
52+
this.detect();
53+
},
54+
filter(Boolean),
55+
);
4556
}
4657

4758
goToWebNode(): void {
48-
this.router.navigate([getFirstFeature()]);
59+
this.router.navigate([Routes.LOADING_WEB_NODE]);
4960
this.initAppFunctionalities();
5061
}
5162

frontend/src/app/app.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { APP_INITIALIZER, ErrorHandler, Injectable, LOCALE_ID, NgModule, Provider } from '@angular/core';
1+
import { APP_INITIALIZER, ErrorHandler, Injectable, LOCALE_ID, NgModule } from '@angular/core';
22
import { BrowserModule } from '@angular/platform-browser';
33

44
import { AppComponent } from './app.component';

frontend/src/app/app.routing.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const SNARKS_TITLE: string = APP_TITLE + ' - Snarks';
1414
export const BLOCK_PRODUCTION_TITLE: string = APP_TITLE + ' - Block Production';
1515
export const MEMPOOL_TITLE: string = APP_TITLE + ' - Mempool';
1616
export const BENCHMARKS_TITLE: string = APP_TITLE + ' - Benchmarks';
17+
export const WEBNODE_TITLE: string = APP_TITLE + ' - Web Node';
1718

1819

1920
function generateRoutes(): Routes {
@@ -64,6 +65,11 @@ function generateRoutes(): Routes {
6465
loadChildren: () => import('./features/benchmarks/benchmarks.module').then(m => m.BenchmarksModule),
6566
title: BENCHMARKS_TITLE,
6667
},
68+
{
69+
path: 'loading-web-node',
70+
loadChildren: () => import('./features/webnode/webnode.module').then(m => m.WebnodeModule),
71+
title: WEBNODE_TITLE,
72+
},
6773
];
6874
if (CONFIG.showWebNodeLandingPage) {
6975
routes.push({
@@ -76,7 +82,7 @@ function generateRoutes(): Routes {
7682
...routes,
7783
{
7884
path: '**',
79-
redirectTo: getFirstFeature(),
85+
redirectTo: CONFIG.showWebNodeLandingPage ? '' : getFirstFeature(),
8086
pathMatch: 'full',
8187
},
8288
];

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export class WebNodeService {
1616
private webNodeStartTime: number;
1717
private sentryEvents: any = {};
1818

19+
readonly webnodeProgress$: BehaviorSubject<string> = new BehaviorSubject<string>('');
20+
1921
constructor(private http: HttpClient) {
2022
const basex = base('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
2123
any(window)['bs58btc'] = {
@@ -45,13 +47,15 @@ export class WebNodeService {
4547
switchMap((wasm: any) => from(wasm.default('assets/webnode/pkg/openmina_node_web_bg.wasm')).pipe(map(() => wasm))),
4648
switchMap((wasm) => {
4749
sendSentryEvent('WebNode Wasm loaded. Starting WebNode');
50+
this.webnodeProgress$.next('Loaded');
4851
return from(wasm.run(this.webNodeKeyPair.privateKey));
4952
}),
5053
tap((webnode: any) => {
5154
sendSentryEvent('WebNode Started');
5255
this.webNodeStartTime = Date.now();
5356
(window as any)['webnode'] = webnode;
5457
this.webnode$.next(webnode);
58+
this.webnodeProgress$.next('Started');
5559
}),
5660
catchError((error) => {
5761
sendSentryEvent('WebNode failed to start');
@@ -83,7 +87,6 @@ export class WebNodeService {
8387
switchMap(handle => from(any(handle).state().peers())),
8488
tap((peers) => {
8589
if (!this.sentryEvents.sentNoPeersEvent && Date.now() - this.webNodeStartTime >= 5000 && peers.length === 0) {
86-
console.log('WebNode has no peers after 5 seconds from startup.');
8790
sendSentryEvent('WebNode has no peers after 5 seconds from startup.');
8891
this.sentryEvents.sentNoPeersEvent = true;
8992
}
@@ -96,6 +99,7 @@ export class WebNodeService {
9699
const seconds = (Date.now() - this.webNodeStartTime) / 1000;
97100
sendSentryEvent(`WebNode connected to its first peer after ${seconds}s`);
98101
this.sentryEvents.firstPeerConnected = true;
102+
this.webnodeProgress$.next('Connected');
99103
}
100104
}),
101105
);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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">
3+
<div class="loading-webnode f-500">
4+
With the Web Node, you can produce blocks directly through your browser
5+
</div>
6+
7+
<div class="font-16 tertiary pt-10 mt-10">
8+
@if (!loading[loading.length - 1].loaded) {
9+
Setting up your in-browser Web Node...
10+
} @else {
11+
Web Node is ready
12+
}
13+
</div>
14+
</div>
15+
16+
<div class="mt-16 border-rad-6 bg-container flex-column p-12 w-100">
17+
@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>
26+
</div>
27+
</div>
28+
}
29+
</div>
30+
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+
42+
@if (!loading[loading.length - 1].loaded && errors.length) {
43+
<div class="flex-column w-100 bg-surface p-12 mt-10 border-rad-6">
44+
<div class="font-16 flex-row flex-column-md flex-between align-center flex-start-md"
45+
[ngClass]="errors.length ? 'warn-primary' : 'aware-primary'">
46+
<div>It appears there are some errors.</div>
47+
</div>
48+
<div class="flex-column border-rad-8 warn-secondary monospace break-word">
49+
@for (error of errors; track $index) {
50+
<div class="lh-sm">{{ error }}</div>
51+
}
52+
</div>
53+
</div>
54+
}
55+
</div>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
@import 'openmina';
2+
3+
$green: #59bfb5;
4+
$light-peach: #0d0d0d;
5+
$peach: #acdea0;
6+
$sand: #5bb3fb;
7+
$white: #000000;
8+
9+
.font-16 {
10+
font-size: 16px;
11+
}
12+
13+
.data-wrapper {
14+
max-width: 568px;
15+
16+
.header {
17+
margin-bottom: 56px;
18+
19+
.loading-webnode {
20+
font-size: 24px;
21+
line-height: 32px;
22+
}
23+
}
24+
25+
.mina-icon.aware-primary {
26+
animation: fadeIn 1500ms ease-in-out infinite;
27+
}
28+
}
29+
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%;
44+
}
45+
}
46+
47+
@keyframes fadeIn {
48+
10% {
49+
opacity: 1;
50+
}
51+
38% {
52+
opacity: 0.1;
53+
}
54+
42% {
55+
opacity: 0.1;
56+
}
57+
70% {
58+
opacity: 1;
59+
}
60+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { ChangeDetectionStrategy, Component, NgZone, OnInit } from '@angular/core';
2+
import { untilDestroyed } from '@ngneat/until-destroy';
3+
import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class';
4+
import { WebNodeService } from '@core/services/web-node.service';
5+
import { GlobalErrorHandlerService } from '@openmina/shared';
6+
import { NgClass, NgForOf, NgIf } from '@angular/common';
7+
import { Router } from '@angular/router';
8+
import { getFirstFeature } from '@shared/constants/config';
9+
import { trigger, state, style, transition, animate } from '@angular/animations';
10+
import { switchMap, timer } from 'rxjs';
11+
12+
export interface WebNodeDemoLoadingStep {
13+
name: string;
14+
loaded: boolean;
15+
attempt?: number;
16+
step: number;
17+
}
18+
19+
20+
@Component({
21+
selector: 'mina-web-node-demo-dashboard',
22+
templateUrl: './web-node-demo-dashboard.component.html',
23+
styleUrls: ['./web-node-demo-dashboard.component.scss'],
24+
changeDetection: ChangeDetectionStrategy.OnPush,
25+
host: { class: 'flex-column h-100 w-100 align-center' },
26+
standalone: true,
27+
imports: [
28+
NgClass,
29+
NgIf,
30+
NgForOf,
31+
],
32+
animations: [
33+
trigger('fadeIn', [
34+
state('void', style({ opacity: 0 })),
35+
state('*', style({ opacity: 1 })),
36+
transition('void => *', [
37+
animate('.6s ease-in'),
38+
]),
39+
]),
40+
],
41+
})
42+
export class WebNodeDemoDashboardComponent extends StoreDispatcher implements OnInit {
43+
44+
readonly loading: WebNodeDemoLoadingStep[] = [
45+
{ name: 'Setting up browser for Web Node', loaded: false, step: 1 },
46+
{ name: 'Getting ready to produce blocks', loaded: false, step: 2 },
47+
{ name: 'Connecting directly to Mina network', loaded: false, step: 3 },
48+
];
49+
ready: boolean = false;
50+
errors: string[] = [];
51+
52+
constructor(private errorHandler: GlobalErrorHandlerService,
53+
private webNodeService: WebNodeService,
54+
private zone: NgZone,
55+
private router: Router) { super(); }
56+
57+
ngOnInit(): void {
58+
this.listenToErrorIssuing();
59+
this.checkWebNodeProgress();
60+
this.fetchPeersInformation();
61+
}
62+
63+
private checkWebNodeProgress(): void {
64+
this.webNodeService.webnodeProgress$.pipe(untilDestroyed(this)).subscribe((state: string) => {
65+
if (state === 'Loaded') {
66+
this.loading[0].loaded = true;
67+
} else if (state === 'Started') {
68+
this.loading[0].loaded = true;
69+
this.loading[1].loaded = true;
70+
} else if (state === 'Connected') {
71+
this.loading.forEach((step: WebNodeDemoLoadingStep) => step.loaded = true);
72+
}
73+
this.ready = this.loading.every((step: WebNodeDemoLoadingStep) => step.loaded);
74+
this.detect();
75+
});
76+
}
77+
78+
private fetchPeersInformation(): void {
79+
timer(0, 1000).pipe(
80+
switchMap(() => this.webNodeService.peers$),
81+
untilDestroyed(this),
82+
).subscribe();
83+
}
84+
85+
private listenToErrorIssuing(): void {
86+
this.errorHandler.errors$
87+
.pipe(untilDestroyed(this))
88+
.subscribe((error: string) => {
89+
console.log(error);
90+
});
91+
}
92+
93+
goToDashboard(): void {
94+
if (!this.ready) {
95+
return;
96+
}
97+
this.router.navigate([getFirstFeature()]);
98+
}
99+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<mina-web-node-demo-dashboard></mina-web-node-demo-dashboard>

frontend/src/app/features/webnode/webnode.component.scss

Whitespace-only changes.

0 commit comments

Comments
 (0)