diff --git a/.drone.yml b/.drone.yml index ed34bb4ea..9e1107ed0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -304,7 +304,7 @@ steps: DOCKER_BUILDKIT: "0" commands: - COMMIT_SHORT_SHA=$(echo $DRONE_COMMIT_SHA | cut -c 1-7) - - docker build --no-cache -t openmina/rust-fe:$COMMIT_SHORT_SHA -f Dockerfile_FE . + - docker build --no-cache -t openmina/frontend:$COMMIT_SHORT_SHA -f Dockerfile_FE . volumes: - name: docker_sock path: /var/run/docker.sock @@ -319,7 +319,7 @@ steps: - build - name: frontend-server - image: openmina/rust-fe:${DRONE_COMMIT_SHA:0:7} + image: openmina/frontend:${DRONE_COMMIT_SHA:0:7} pull: if-not-exists detach: true privileged: true @@ -449,7 +449,7 @@ steps: image: docker:latest commands: - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin - - docker push openmina/rust-fe:${DRONE_COMMIT_SHA:0:7} + - docker push openmina/frontend:${DRONE_COMMIT_SHA:0:7} volumes: - name: docker_sock path: /var/run/docker.sock diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index af0cc1ca0..b55b520c2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -47,18 +47,6 @@ jobs: rustup override set 1.77 rustup component add clippy rustfmt - # - name: Check - # uses: actions-rs/cargo@v1 - # with: - # command: check - - # - name: Clippy - # uses: actions-rs/cargo@v1 - # with: - # command: clippy - # # don't fail the job until clippy is fixed - # continue-on-error: true - - name: Setup Rust Cache uses: Swatinem/rust-cache@v2 with: @@ -74,9 +62,26 @@ jobs: name: bin path: target/release/openmina + build-tests: + runs-on: ubuntu-20.04 + steps: + - name: Git checkout + uses: actions/checkout@v4 + + - name: Setup Rust + run: | + rustup install 1.77 + rustup override set 1.77 + rustup component add clippy rustfmt + + - name: Setup Rust Cache + uses: Swatinem/rust-cache@v2 + with: + prefix-key: "ver-0" + - name: Build tests run: | - mkdir target/release/tests + mkdir -p target/release/tests cargo build --release --features=scenario-generators --package=openmina-node-testing --tests cargo build --release --features=scenario-generators --package=openmina-node-testing --tests --message-format=json > cargo-build-test.json @@ -102,7 +107,7 @@ jobs: path: tests.tar p2p-tests: - needs: [ build ] + needs: [ build-tests ] runs-on: ubuntu-20.04 container: image: minaprotocol/mina-daemon:2.0.0berkeley-rc1-1551e2f-focal-berkeley @@ -131,7 +136,7 @@ jobs: ./${{ env.TEST }} --nocapture --test-threads=1 scenario-tests: - needs: [ build ] + needs: [ build-tests ] runs-on: ubuntu-20.04 container: image: minaprotocol/mina-daemon:2.0.0berkeley-rc1-1551e2f-focal-berkeley @@ -158,3 +163,56 @@ jobs: - name: Run the test run: | ./${{ matrix.test }} --nocapture --test-threads=1 + + + bootstrap-test: + needs: [ build ] + runs-on: ubuntu-20.04 + steps: + - name: Download binary + uses: actions/download-artifact@v4 + with: + name: bin + + - name: Prepare peers list + run: | + cat > peers.txt < [u8; 32] { psk_fixed.copy_from_slice(hash.as_ref()); psk_fixed } + +pub use log::ActionEvent; +pub use openmina_macros::*; diff --git a/core/src/log.rs b/core/src/log.rs index cb1db0d83..15ec29349 100644 --- a/core/src/log.rs +++ b/core/src/log.rs @@ -73,4 +73,71 @@ macro_rules! error { }; } +pub const ACTION_TRACE_TARGET: &str = "openmina_core::log::action"; + +#[macro_export] +macro_rules! action_event { + ($level:expr, $context:expr, $($tts:tt)*) => { + $crate::log::inner::event!(target: { $crate::log::ACTION_TRACE_TARGET }, $level, time = $context.time(), kind = $context.kind(), node_id = $context.node_id(), $($tts)*) + }; + ($level:expr, $context:expr) => { + $crate::log::inner::event!(target: { $crate::log::ACTION_TRACE_TARGET }, $level, time = $context.time(), kind = $context.kind(), node_id = $context.node_id()) + }; + } + +#[macro_export] +macro_rules! action_warn { + ($context:expr, $($tts:tt)*) => { + $crate::action_event!($crate::log::inner::Level::WARN, $context, $($tts)*) + }; + ($context:expr) => { + $crate::action_event!($crate::log::inner::Level::WARN, $context) + }; +} + +#[macro_export] +macro_rules! action_info { + ($context:expr, $($tts:tt)*) => { + $crate::action_event!($crate::log::inner::Level::INFO, $context, $($tts)*) + }; + ($context:expr) => { + $crate::action_event!($crate::log::inner::Level::INFO, $context) + }; +} + +#[macro_export] +macro_rules! action_debug { + ($context:expr, $($tts:tt)*) => { + $crate::action_event!($crate::log::inner::Level::DEBUG, $context, $($tts)*) + }; + ($context:expr) => { + $crate::action_event!($crate::log::inner::Level::DEBUG, $context) + }; +} + +#[macro_export] +macro_rules! action_trace { + ($context:expr, $($tts:tt)*) => { + $crate::action_event!($crate::log::inner::Level::TRACE, $context, $($tts)*) + }; + ($context:expr) => { + $crate::action_event!($crate::log::inner::Level::TRACE, $context) + }; +} + +pub trait EventContext { + fn timestamp(&self) -> redux::Timestamp; + fn time(&self) -> &'_ dyn Value; + fn node_id(&self) -> &'_ dyn Value; + fn kind(&self) -> &'_ dyn Value; +} + +pub trait ActionEvent { + fn action_event(&self, context: &T) + where + T: EventContext; +} + +use tracing::Value; + pub use crate::{debug, error, info, trace, warn}; diff --git a/core/src/snark/snark_job_commitment.rs b/core/src/snark/snark_job_commitment.rs index b071e1d34..ab02ee115 100644 --- a/core/src/snark/snark_job_commitment.rs +++ b/core/src/snark/snark_job_commitment.rs @@ -10,9 +10,7 @@ use super::SnarkJobId; #[derive(BinProtWrite, BinProtRead, Serialize, Deserialize, Debug, Clone)] pub struct SnarkJobCommitment { - /// Timestamp in milliseconds. - /// TODO(binier): have to use i64, because binprot doesn't support u64. - timestamp: i64, + timestamp: u64, pub job_id: SnarkJobId, pub fee: CurrencyFeeStableV1, pub snarker: NonZeroCurvePoint, @@ -27,7 +25,7 @@ impl SnarkJobCommitment { snarker: NonZeroCurvePoint, ) -> Self { Self { - timestamp: timestamp as i64, + timestamp, job_id, fee, snarker, diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 000000000..b3b67113e --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,29 @@ +version: '3.8' + +services: + openmina_node: + build: + context: ./ + dockerfile: Dockerfile + command: [ "node" ] + ports: + - "3000:3000" + environment: + - MINA_SNARK_WORKER_TAG=0.0.9 + networks: + - app-network + + frontend: + build: + context: ./ + dockerfile: Dockerfile_FE + args: + BUILD_CONFIGURATION: local + ports: + - "8070:80" + networks: + - app-network + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b3b67113e..2b0e78a5a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,29 +1,15 @@ version: '3.8' services: - openmina_node: - build: - context: ./ - dockerfile: Dockerfile + openmina-node: + image: openmina/openmina:${OPENMINA_TAG:-latest} command: [ "node" ] ports: - "3000:3000" environment: - MINA_SNARK_WORKER_TAG=0.0.9 - networks: - - app-network frontend: - build: - context: ./ - dockerfile: Dockerfile_FE - args: - BUILD_CONFIGURATION: local + image: openmina/frontend:${OPENMINA_FRONTEND_TAG:-latest} ports: - "8070:80" - networks: - - app-network - -networks: - app-network: - driver: bridge \ No newline at end of file diff --git a/frontend/.drone.yml b/frontend/.drone.yml index 4b1172d9f..8409395ed 100644 --- a/frontend/.drone.yml +++ b/frontend/.drone.yml @@ -17,7 +17,7 @@ steps: DOCKER_BUILDKIT: "0" commands: - COMMIT_SHORT_SHA=$(echo $DRONE_COMMIT_SHA | cut -c 1-7) - - docker build --no-cache -t directcuteo/rust-fe:$COMMIT_SHORT_SHA -f Dockerfile . + - docker build --no-cache -t openmina/frontend:$COMMIT_SHORT_SHA -f Dockerfile . volumes: - name: docker_sock path: /var/run/docker.sock @@ -28,7 +28,7 @@ steps: image: docker:latest commands: - echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin - - docker push directcuteo/rust-fe:${DRONE_COMMIT_SHA:0:7} + - docker push openmina/frontend:${DRONE_COMMIT_SHA:0:7} volumes: - name: docker_sock path: /var/run/docker.sock diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 7bbadae87..4e8593606 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -50,14 +50,15 @@ #COPY nginx.conf . #RUN cat nginx.conf - FROM node:18 AS BUILD_IMAGE +ARG BUILD_CONFIGURATION=production WORKDIR /app -COPY dist/ ./dist/ -COPY nginx.conf . -#RUN npm install -#RUN node_modules/.bin/ng build --configuration production -#RUN npm prune --production +COPY . . +RUN npm install +RUN node_modules/.bin/ng build --configuration ${BUILD_CONFIGURATION} +RUN npm prune --production +RUN [ -f nginx.${BUILD_CONFIGURATION}.conf ] && cp nginx.${BUILD_CONFIGURATION}.conf nginx.conf + FROM nginx:alpine RUN pwd RUN ls -l diff --git a/frontend/angular.json b/frontend/angular.json index 0ebb71e50..ee99e3d1e 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -81,6 +81,15 @@ } ], "outputHashing": "all" + }, + "compose": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.compose.ts" + } + ], + "outputHashing": "all" } }, "defaultConfiguration": "production" diff --git a/frontend/nginx.compose.conf b/frontend/nginx.compose.conf new file mode 100644 index 000000000..2b6e34154 --- /dev/null +++ b/frontend/nginx.compose.conf @@ -0,0 +1,47 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + + server { + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html; + index index.html index.htm; + error_page 404 /usr/share/nginx/html/index.html; + } + + location /openmina-node { + rewrite_log on; + rewrite ^/openmina-node/(.*) /$1 break; + proxy_pass http://openmina-node:3000; + } + } + + include /etc/nginx/mime.types; + default_type application/octet-stream; + large_client_header_buffers 4 32k; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + #gzip on; + + # include /etc/nginx/conf.d/*.conf; +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f7bb4485..9263e3353 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,7 +22,7 @@ "@ngrx/effects": "^16.2.0", "@ngrx/router-store": "^16.2.0", "@ngrx/store": "^16.2.0", - "@openmina/shared": "^0.66.0", + "@openmina/shared": "^0.71.0", "d3": "^7.8.4", "eigen": "^0.2.2", "mathjs": "^12.3.0", @@ -3692,27 +3692,27 @@ } }, "node_modules/@openmina/shared": { - "version": "0.66.0", - "resolved": "https://registry.npmjs.org/@openmina/shared/-/shared-0.66.0.tgz", - "integrity": "sha512-c9JNU/qPVj9Rchcs0Hhd8Ddj5l7RZSQeK5GytQm1SN/4HxbqMOpvlSh9kZG2xK/zo/QOsvXf93IXK54w3QQGRg==", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "@angular/animations": "^16.2.11", - "@angular/common": "^16.2.11", - "@angular/core": "^16.2.11", - "@angular/forms": "^16.2.11", - "@angular/material": "^16.2.10", - "@ngneat/until-destroy": "^10.0.0", - "@ngrx/effects": "^16.2.0", - "@ngrx/router-store": "^16.2.0", - "@ngrx/store": "^16.2.0", - "@ngrx/store-devtools": "^16.2.0", - "d3": "^7.8.4", - "ngx-json-viewer": "^3.2.1", - "rxjs": "~7.8.0", - "zone.js": "~0.13.0" + "version": "0.71.0", + "resolved": "https://registry.npmjs.org/@openmina/shared/-/shared-0.71.0.tgz", + "integrity": "sha512-hxpExaYDLat2wB/wldLm6z0lc7BP4rUROVcU74+b1b0e7UVq5x3LGQJFbbpiwuEuLGHKaeggMZrAF2v/jpaIjg==", + "dependencies": { + "tslib": ">=2.3.0" + }, + "peerDependencies": { + "@angular/animations": ">=16.2.11", + "@angular/common": ">=16.2.11", + "@angular/core": ">=16.2.11", + "@angular/forms": ">=16.2.11", + "@angular/material": ">=16.2.10", + "@ngneat/until-destroy": ">=10.0.0", + "@ngrx/effects": ">=16.2.0", + "@ngrx/router-store": ">=16.2.0", + "@ngrx/store": ">=16.2.0", + "@ngrx/store-devtools": ">=16.2.0", + "d3": ">=7.8.4", + "ngx-json-viewer": ">=3.2.1", + "rxjs": ">=7.8.0", + "zone.js": ">=0.13.0" } }, "node_modules/@pkgjs/parseargs": { diff --git a/frontend/package.json b/frontend/package.json index 79a7f0393..7a7bfd6ff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "watch": "ng build --watch --configuration development", "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 directcuteo/rust-fe:latest . && docker push directcuteo/rust-fe:latest" + "docker": "npm run build:prod && docker buildx build --platform linux/amd64 -t openmina/frontend:latest . && docker push openmina/frontend:latest" }, "private": true, "dependencies": { @@ -28,7 +28,7 @@ "@ngrx/effects": "^16.2.0", "@ngrx/router-store": "^16.2.0", "@ngrx/store": "^16.2.0", - "@openmina/shared": "^0.66.0", + "@openmina/shared": "^0.71.0", "d3": "^7.8.4", "eigen": "^0.2.2", "mathjs": "^12.3.0", diff --git a/frontend/src/app/app.actions.ts b/frontend/src/app/app.actions.ts index 14170e082..c6cb69263 100644 --- a/frontend/src/app/app.actions.ts +++ b/frontend/src/app/app.actions.ts @@ -1,10 +1,11 @@ import { FeatureAction } from '@openmina/shared'; -import { MinaNode } from '@shared/types/core/environment/mina-env.type'; +import { FeaturesConfig, MinaNode } from '@shared/types/core/environment/mina-env.type'; enum AppActionTypes { APP_INIT = 'APP_INIT', APP_INIT_SUCCESS = 'APP_INIT_SUCCESS', APP_CHANGE_ACTIVE_NODE = 'APP_CHANGE_ACTIVE_NODE', + APP_DELETE_NODE = 'APP_DELETE_NODE', APP_ADD_NODE = 'APP_ADD_NODE', APP_CHANGE_MENU_COLLAPSING = 'APP_CHANGE_MENU_COLLAPSING', APP_CHANGE_SUB_MENUS = 'APP_CHANGE_SUB_MENUS', @@ -15,6 +16,7 @@ enum AppActionTypes { export const APP_INIT = AppActionTypes.APP_INIT; export const APP_INIT_SUCCESS = AppActionTypes.APP_INIT_SUCCESS; export const APP_CHANGE_ACTIVE_NODE = AppActionTypes.APP_CHANGE_ACTIVE_NODE; +export const APP_DELETE_NODE = AppActionTypes.APP_DELETE_NODE; export const APP_ADD_NODE = AppActionTypes.APP_ADD_NODE; export const APP_CHANGE_MENU_COLLAPSING = AppActionTypes.APP_CHANGE_MENU_COLLAPSING; export const APP_CHANGE_SUB_MENUS = AppActionTypes.APP_CHANGE_SUB_MENUS; @@ -41,10 +43,16 @@ export class AppChangeActiveNode implements AppAction { constructor(public payload: MinaNode) { } } +export class AppDeleteNode implements AppAction { + readonly type = APP_DELETE_NODE; + + constructor(public payload: MinaNode) { } +} + export class AppAddNode implements AppAction { readonly type = APP_ADD_NODE; - constructor(public payload: string) { } + constructor(public payload: MinaNode) { } } export class AppChangeMenuCollapsing implements AppAction { @@ -74,6 +82,7 @@ export type AppActions = | AppInitSuccess | AppAddNode | AppChangeActiveNode + | AppDeleteNode | AppChangeMenuCollapsing | AppChangeSubMenus | AppToggleMobile diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss index 8fb06ed3d..89dedd166 100644 --- a/frontend/src/app/app.component.scss +++ b/frontend/src/app/app.component.scss @@ -66,10 +66,10 @@ mat-sidenav-content { $toolbar: 56px; $subMenus: 56px; height: calc(100% - #{$toolbar} - #{$subMenus}); - } - &.no-submenus { - height: calc(100% - #{$toolbar}); + &.no-submenus { + height: calc(100% - #{$toolbar}); + } } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 9937e7fd3..f0337190f 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -9,7 +9,7 @@ import { HorizontalMenuComponent, NgrxRouterStoreModule, OpenminaEagerSharedModule, - THEME_PROVIDER + THEME_PROVIDER, } from '@openmina/shared'; import { CommonModule, registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; @@ -29,6 +29,8 @@ import { EffectsModule } from '@ngrx/effects'; import { AppEffects } from '@app/app.effects'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { HttpClientModule } from '@angular/common/http'; +import { NewNodeComponent } from './layout/new-node/new-node.component'; +import { ReactiveFormsModule } from '@angular/forms'; registerLocaleData(localeFr, 'fr'); registerLocaleData(localeEn, 'en'); @@ -53,6 +55,7 @@ export class AppGlobalErrorhandler implements ErrorHandler { ServerStatusComponent, SubmenuTabsComponent, NodePickerComponent, + NewNodeComponent, ], imports: [ CommonModule, @@ -75,12 +78,13 @@ export class AppGlobalErrorhandler implements ErrorHandler { MatSidenavModule, OpenminaEagerSharedModule, HorizontalMenuComponent, + ReactiveFormsModule, ], providers: [ THEME_PROVIDER, { provide: ErrorHandler, useClass: AppGlobalErrorhandler, deps: [GlobalErrorHandlerService] }, { provide: LOCALE_ID, useValue: 'en' }, ], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) export class AppModule {} diff --git a/frontend/src/app/app.reducer.ts b/frontend/src/app/app.reducer.ts index 27246516b..699dfa5b0 100644 --- a/frontend/src/app/app.reducer.ts +++ b/frontend/src/app/app.reducer.ts @@ -1,7 +1,7 @@ import { APP_ADD_NODE, APP_CHANGE_ACTIVE_NODE, - APP_CHANGE_MENU_COLLAPSING, + APP_CHANGE_MENU_COLLAPSING, APP_DELETE_NODE, APP_INIT, APP_INIT_SUCCESS, APP_TOGGLE_MENU_OPENING, @@ -10,7 +10,6 @@ import { import { AppState } from '@app/app.state'; import { MinaNode } from '@shared/types/core/environment/mina-env.type'; - const initialState: AppState = { menu: { collapsed: JSON.parse(localStorage.getItem('menu_collapsed')) || false, @@ -78,22 +77,22 @@ export function appReducer(state: AppState = initialState, action: any): AppStat } case APP_ADD_NODE: { - const newNode: MinaNode = { - features: {//todo: add this - // overview: ['nodes', 'topology'], - // explorer: ['blocks', 'transactions', 'snark-pool', 'scan-state', 'snark-traces'], - // resources: ['system'], - // network: ['messages', 'connections', 'blocks', 'blocks-ipc'], - // tracing: ['overview', 'blocks'], - // benchmarks: ['wallets'], - // 'web-node': ['wallet', 'peers', 'logs', 'state'], - }, - url: action.payload, - name: action.payload.split('/')[action.payload.split('/').length - 1] || ('custom-node' + ++state.nodes.filter(n => n.name.includes('custom-node')).length), + const customNodes = localStorage.getItem('custom_nodes') ? JSON.parse(localStorage.getItem('custom_nodes')) : []; + localStorage.setItem('custom_nodes', JSON.stringify([action.payload, ...customNodes])); + return { + ...state, + nodes: [action.payload, ...state.nodes], }; + } + + case APP_DELETE_NODE: { + const customNodes = localStorage.getItem('custom_nodes') ? JSON.parse(localStorage.getItem('custom_nodes')) : []; + localStorage.setItem('custom_nodes', JSON.stringify(customNodes.filter((node: MinaNode) => node.name !== action.payload.name))); + const nodes = state.nodes.filter(node => node.name !== action.payload.name); return { ...state, - nodes: [newNode, ...state.nodes], + nodes, + activeNode: state.activeNode?.name === action.payload.name ? nodes[0] : state.activeNode, }; } diff --git a/frontend/src/app/app.routing.ts b/frontend/src/app/app.routing.ts index 7098f2971..a0d68848a 100644 --- a/frontend/src/app/app.routing.ts +++ b/frontend/src/app/app.routing.ts @@ -11,6 +11,7 @@ export const NODES_TITLE: string = APP_TITLE + ' - Nodes'; export const STATE_TITLE: string = APP_TITLE + ' - State'; export const SNARKS_TITLE: string = APP_TITLE + ' - Snarks'; export const TESTING_TOOL_TITLE: string = APP_TITLE + ' - Testing Tool'; +export const BLOCK_PRODUCTION_TITLE: string = APP_TITLE + ' - Block Production'; const routes: Routes = [ @@ -50,6 +51,11 @@ const routes: Routes = [ loadChildren: () => import('@testing-tool/testing-tool.module').then(m => m.TestingToolModule), title: TESTING_TOOL_TITLE, }, + { + path: 'block-production', + loadChildren: () => import('./features/block-production/block-production.module').then(m => m.BlockProductionModule), + title: BLOCK_PRODUCTION_TITLE, + }, { path: '**', redirectTo: getFirstFeature(), diff --git a/frontend/src/app/app.service.ts b/frontend/src/app/app.service.ts index 7506b4afa..44e325dc4 100644 --- a/frontend/src/app/app.service.ts +++ b/frontend/src/app/app.service.ts @@ -16,6 +16,9 @@ export class AppService { } getNodes(): Observable { - return of(CONFIG.configs); + return of([ + ...CONFIG.configs, + ...(localStorage.getItem('custom_nodes') ? JSON.parse(localStorage.getItem('custom_nodes')) : []), + ]); } } diff --git a/frontend/src/app/features/block-production/block-production.component.html b/frontend/src/app/features/block-production/block-production.component.html new file mode 100644 index 000000000..0680b43f9 --- /dev/null +++ b/frontend/src/app/features/block-production/block-production.component.html @@ -0,0 +1 @@ + diff --git a/frontend/src/app/features/network/node-dht/network-node-dht-bootstrap-details/network-node-dht-bootstrap-details.component.scss b/frontend/src/app/features/block-production/block-production.component.scss similarity index 100% rename from frontend/src/app/features/network/node-dht/network-node-dht-bootstrap-details/network-node-dht-bootstrap-details.component.scss rename to frontend/src/app/features/block-production/block-production.component.scss diff --git a/frontend/src/app/features/block-production/block-production.component.ts b/frontend/src/app/features/block-production/block-production.component.ts new file mode 100644 index 000000000..a772c4879 --- /dev/null +++ b/frontend/src/app/features/block-production/block-production.component.ts @@ -0,0 +1,11 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'mina-block-production', + templateUrl: './block-production.component.html', + styleUrls: ['./block-production.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BlockProductionComponent { + +} diff --git a/frontend/src/app/features/block-production/block-production.module.ts b/frontend/src/app/features/block-production/block-production.module.ts new file mode 100644 index 000000000..171c80a24 --- /dev/null +++ b/frontend/src/app/features/block-production/block-production.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BlockProductionRouting } from './block-production.routing'; +import { BlockProductionComponent } from './block-production.component'; + + +@NgModule({ + declarations: [ + BlockProductionComponent, + ], + imports: [ + CommonModule, + BlockProductionRouting, + ], +}) +export class BlockProductionModule {} diff --git a/frontend/src/app/features/block-production/block-production.routing.ts b/frontend/src/app/features/block-production/block-production.routing.ts new file mode 100644 index 000000000..52aa8ae93 --- /dev/null +++ b/frontend/src/app/features/block-production/block-production.routing.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { BlockProductionComponent } from './block-production.component'; +import { BLOCK_PRODUCTION_TITLE } from '@app/app.routing'; + +const routes: Routes = [ + { + path: '', + component: BlockProductionComponent, + title: BLOCK_PRODUCTION_TITLE, + children: [ + { + path: 'overview', + loadChildren: () => import('./overview/block-production-overview.module').then(m => m.BlockProductionOverviewModule), + }, + { + path: '', + pathMatch: 'full', + redirectTo: 'overview', + }, + ], + }, + { + path: '**', + redirectTo: '', + pathMatch: 'full', + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class BlockProductionRouting {} diff --git a/frontend/src/app/features/block-production/overview/block-production-overview.component.html b/frontend/src/app/features/block-production/overview/block-production-overview.component.html new file mode 100644 index 000000000..d655972ae --- /dev/null +++ b/frontend/src/app/features/block-production/overview/block-production-overview.component.html @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/frontend/src/app/features/network/node-dht/network-node-dht-peer-details/network-node-dht-peer-details.component.scss b/frontend/src/app/features/block-production/overview/block-production-overview.component.scss similarity index 100% rename from frontend/src/app/features/network/node-dht/network-node-dht-peer-details/network-node-dht-peer-details.component.scss rename to frontend/src/app/features/block-production/overview/block-production-overview.component.scss diff --git a/frontend/src/app/features/block-production/overview/block-production-overview.component.ts b/frontend/src/app/features/block-production/overview/block-production-overview.component.ts new file mode 100644 index 000000000..c13d8ea7a --- /dev/null +++ b/frontend/src/app/features/block-production/overview/block-production-overview.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, ElementRef, OnInit } from '@angular/core'; +import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; +import { + BlockProductionOverviewService, +} from '@app/features/block-production/overview/block-production-overview.service'; + +@Component({ + selector: 'mina-block-production-overview', + templateUrl: './block-production-overview.component.html', + styleUrls: ['./block-production-overview.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlockProductionOverviewComponent extends StoreDispatcher implements OnInit { + + constructor(protected el: ElementRef) {super();} + + ngOnInit(): void { + + } +} diff --git a/frontend/src/app/features/block-production/overview/block-production-overview.module.ts b/frontend/src/app/features/block-production/overview/block-production-overview.module.ts new file mode 100644 index 000000000..c5bcaf3eb --- /dev/null +++ b/frontend/src/app/features/block-production/overview/block-production-overview.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BlockProductionOverviewRouting } from './block-production-overview.routing'; +import { BlockProductionOverviewComponent } from './block-production-overview.component'; +import { BlockProductionSlotsComponent } from './block-production-slots/block-production-slots.component'; +import { HorizontalResizableContainerComponent } from '@openmina/shared'; + + +@NgModule({ + declarations: [ + BlockProductionOverviewComponent, + BlockProductionSlotsComponent, + ], + imports: [ + CommonModule, + BlockProductionOverviewRouting, + HorizontalResizableContainerComponent, + ], +}) +export class BlockProductionOverviewModule {} diff --git a/frontend/src/app/features/block-production/overview/block-production-overview.routing.ts b/frontend/src/app/features/block-production/overview/block-production-overview.routing.ts new file mode 100644 index 000000000..000e1a32e --- /dev/null +++ b/frontend/src/app/features/block-production/overview/block-production-overview.routing.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { + BlockProductionOverviewComponent, +} from '@app/features/block-production/overview/block-production-overview.component'; +import { BLOCK_PRODUCTION_TITLE } from '@app/app.routing'; + +const routes: Routes = [ + { + path: '', + component: BlockProductionOverviewComponent, + title: BLOCK_PRODUCTION_TITLE, + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class BlockProductionOverviewRouting {} diff --git a/frontend/src/app/features/block-production/overview/block-production-overview.service.ts b/frontend/src/app/features/block-production/overview/block-production-overview.service.ts new file mode 100644 index 000000000..0912a287f --- /dev/null +++ b/frontend/src/app/features/block-production/overview/block-production-overview.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@angular/core'; +import { BlockProductionModule } from '@app/features/block-production/block-production.module'; +import { Observable, of } from 'rxjs'; + +export interface BlockProductionSlot { + height: number; + time: number; + finished: boolean; + canonical: boolean; + orphaned: boolean; + missed: boolean; + missedRights: boolean; + futureRights: boolean; + active: boolean; +} + +@Injectable({ + providedIn: BlockProductionModule, +}) +export class BlockProductionOverviewService { + + constructor() { } + + getSlots(): Observable { + return of(this.generateRandomSlots()); + } + + private generateRandomSlots(): BlockProductionSlot[] { + // generate slots interval 6732–7300 + // with random values + // height is the index starting from 6732 + const slots = []; + for (let i = 6732; i < 11000; i++) { + slots.push({ + height: i, + time: Math.floor(Math.random() * 100), + finished: true, + canonical: Math.random() > 0.95, + orphaned: Math.random() > 0.95, + missed: Math.random() > 0.95, + missedRights: Math.random() > 0.99, + futureRights: false, + active: i === 10999, + }); + } + // rest push only slots where only futureRights can be true + for (let i = 10000; i < 14010; i++) { + slots.push({ + height: i, + time: Math.floor(Math.random() * 100), + finished: false, + canonical: false, + orphaned: false, + missed: false, + missedRights: false, + futureRights: Math.random() > 0.98, + active: false, + }); + } + return slots; + } +} diff --git a/frontend/src/app/features/block-production/overview/block-production-slots/block-production-slots.component.html b/frontend/src/app/features/block-production/overview/block-production-slots/block-production-slots.component.html new file mode 100644 index 000000000..b65156be6 --- /dev/null +++ b/frontend/src/app/features/block-production/overview/block-production-slots/block-production-slots.component.html @@ -0,0 +1,6 @@ +
+ + diff --git a/frontend/src/app/features/block-production/overview/block-production-slots/block-production-slots.component.scss b/frontend/src/app/features/block-production/overview/block-production-slots/block-production-slots.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/features/block-production/overview/block-production-slots/block-production-slots.component.ts b/frontend/src/app/features/block-production/overview/block-production-slots/block-production-slots.component.ts new file mode 100644 index 000000000..1f2c9d793 --- /dev/null +++ b/frontend/src/app/features/block-production/overview/block-production-slots/block-production-slots.component.ts @@ -0,0 +1,123 @@ +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; +import { + BlockProductionOverviewService, BlockProductionSlot, +} from '@app/features/block-production/overview/block-production-overview.service'; +import * as d3 from 'd3'; + +@Component({ + selector: 'mina-block-production-slots', + templateUrl: './block-production-slots.component.html', + styleUrls: ['./block-production-slots.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'flex-column h-100' }, +}) +export class BlockProductionSlotsComponent extends StoreDispatcher implements AfterViewInit { + @ViewChild('graph') private graph: ElementRef; + @ViewChild('tooltip') private tooltipRef: ElementRef; + private tooltip: d3.Selection; + + // Define margins as a property of the class + private margin = { top: 0, right: 0, bottom: 0, left: 0 }; + private width: number; + private height: number; + private slots: BlockProductionSlot[] = []; + + constructor(private service: BlockProductionOverviewService) { super(); } + + ngAfterViewInit(): void { + this.service.getSlots().subscribe(slots => { + this.slots = slots; + this.initSlots(); + }); + } + + private initSlots(): void { + this.width = this.graph.nativeElement.clientWidth; + this.height = this.graph.nativeElement.clientHeight; + this.tooltip = d3.select(this.tooltipRef.nativeElement); + + const svg = d3.select(this.graph.nativeElement) + .append('svg') + .attr('width', this.width + this.margin.left + this.margin.right) + .attr('height', this.height + this.margin.top + this.margin.bottom); + + const slotsGroup = svg.append('g'); + const slotWidth = 8; + const slotHeight = 8; + const slotMargin = 1; + const slotsPerRow = Math.floor(this.width / (slotWidth + slotMargin)); + const slotGroup = slotsGroup.selectAll('rect') + .data(this.slots) + .enter() + .append('rect') + .attr('width', slotWidth) + .attr('height', slotHeight) + .attr('x', (d, i) => { + return (i % slotsPerRow) * (slotWidth + slotMargin); + }) + .attr('y', (d, i) => { + const rowIndex = Math.floor(i / slotsPerRow); + return rowIndex * (slotHeight + slotMargin); + }) + + .attr('fill', d => { + const prefix = 'var(--'; + const suffix = ')'; + let color = 'base-container'; + if (d.finished) { + color = 'selected-tertiary'; + } + + if (d.canonical) { + color = 'success-primary'; + } else if (d.orphaned) { + color = 'special-selected-alt-1-primary'; + } else if (d.missedRights) { + color = 'warn-primary'; + } else if (d.futureRights) { + color = 'base-secondary'; + } else if (d.missed) { + return undefined; + } else if (!d.finished) { + color = 'base-container'; + } + + if (d.active) { + color = 'selected-primary'; + } + return `${prefix}${color}${suffix}`; + }) + // add tooltip on hover with color + .on('mouseover', (event: MouseEvent & { + target: HTMLElement + }, d: BlockProductionSlot) => this.mouseOverHandle(event, d)) + .on('mouseout', (event: MouseEvent & { + target: HTMLElement + }, d: BlockProductionSlot) => this.mouseOutHandler(event, d)); + } + + private mouseOverHandle(event: MouseEvent & { target: HTMLElement }, d: BlockProductionSlot): void { + const color = event.target.getAttribute('fill'); + const selection = this.tooltip.html(` +
+
${d.height}
+
+ `) + .style('display', 'block'); + + const nodeRect = event.target.getBoundingClientRect(); + const tooltipWidth = selection.node().getBoundingClientRect().width; + + const chartLeft = this.graph.nativeElement.getBoundingClientRect().left; + let desiredLeft = Math.min(nodeRect.left + nodeRect.width / 2 - tooltipWidth / 2, chartLeft + this.width - tooltipWidth); + desiredLeft = Math.max(desiredLeft, chartLeft); + selection + .style('left', `${desiredLeft}px`) + .style('top', `${nodeRect.bottom + window.scrollY + 12}px`); + } + + private mouseOutHandler(event: MouseEvent & { target: HTMLElement }, d: BlockProductionSlot): void { + this.tooltip.style('display', 'none'); + } +} diff --git a/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.html b/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.html index b886e0349..3c9edc61b 100644 --- a/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.html +++ b/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.html @@ -1,8 +1,6 @@
-
- Last 290 blocks -
+
Last 290 blocks
{{ syncProgress }}
diff --git a/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts b/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts index 1f371f34b..5ef7aaf2c 100644 --- a/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts +++ b/frontend/src/app/features/dashboard/dashboard-blocks-sync/dashboard-blocks-sync.component.ts @@ -62,7 +62,7 @@ export class DashboardBlocksSyncComponent extends StoreDispatcher implements OnI this.root = lastItem(blocks).height; this.rootText = this.calculateProgressTime(lastItem(blocks).applyEnd); if (blocks[0].status === NodesOverviewNodeBlockStatus.APPLIED) { - this.bestTipBlockSyncedText = SYNCED + ' ' + this.bestTipBlockSyncedText; + this.bestTipBlockSyncedText = SYNCED + ' ' + this.bestTipBlockSyncedText.slice(7); } } else { this.root = null; @@ -80,12 +80,9 @@ export class DashboardBlocksSyncComponent extends StoreDispatcher implements OnI if (!timestamp) { return 'Pending'; } - timestamp = timestamp / ONE_MILLION; - // const timestampDate = new Date(timestamp); - const timezoneOffset = 0; //timestampDate.getTimezoneOffset(); - - const millisecondsAgo = Date.now() - timestamp - timezoneOffset * 60 * 1000; - const minutesAgo = Math.floor(millisecondsAgo / 60000); + timestamp = Math.ceil(timestamp / ONE_MILLION); + const millisecondsAgo = Date.now() - timestamp; + const minutesAgo = Math.floor(millisecondsAgo / 1000 / 60); const hoursAgo = Math.floor(minutesAgo / 60); const daysAgo = Math.floor(hoursAgo / 24); diff --git a/frontend/src/app/features/dashboard/dashboard-errors/dashboard-errors.component.ts b/frontend/src/app/features/dashboard/dashboard-errors/dashboard-errors.component.ts index d52e078f9..28ece4f6e 100644 --- a/frontend/src/app/features/dashboard/dashboard-errors/dashboard-errors.component.ts +++ b/frontend/src/app/features/dashboard/dashboard-errors/dashboard-errors.component.ts @@ -1,13 +1,13 @@ -import {ChangeDetectionStrategy, Component, OnInit} from '@angular/core'; -import {StoreDispatcher} from '@shared/base-classes/store-dispatcher.class'; -import {selectDashboardNodes} from '@dashboard/dashboard.state'; -import {NodesOverviewNode} from '@shared/types/nodes/dashboard/nodes-overview-node.type'; -import {ONE_MILLION} from '@openmina/shared'; -import {filter} from 'rxjs'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { StoreDispatcher } from '@shared/base-classes/store-dispatcher.class'; +import { selectDashboardNodes } from '@dashboard/dashboard.state'; +import { NodesOverviewNode } from '@shared/types/nodes/dashboard/nodes-overview-node.type'; +import { ONE_MILLION } from '@openmina/shared'; +import { filter } from 'rxjs'; import { NodesOverviewResync, NodesOverviewResyncKindType, - NodesOverviewResyncUI + NodesOverviewResyncUI, } from '@shared/types/nodes/dashboard/nodes-overview-resync.type'; const descriptionMap = { @@ -54,12 +54,9 @@ export class DashboardErrorsComponent extends StoreDispatcher implements OnInit } private calculateProgressTime(timestamp: number): string { - timestamp = timestamp / ONE_MILLION; - const timestampDate = new Date(timestamp); - const timezoneOffset = 0;// timestampDate.getTimezoneOffset(); - - const millisecondsAgo = Date.now() - timestamp - timezoneOffset * 60 * 1000; - const minutesAgo = Math.floor(millisecondsAgo / 60000); + timestamp = Math.ceil(timestamp / ONE_MILLION); + const millisecondsAgo = Date.now() - timestamp; + const minutesAgo = Math.floor(millisecondsAgo / 1000 / 60); const hoursAgo = Math.floor(minutesAgo / 60); const daysAgo = Math.floor(hoursAgo / 24); diff --git a/frontend/src/app/features/dashboard/dashboard-ledger/dashboard-ledger.component.html b/frontend/src/app/features/dashboard/dashboard-ledger/dashboard-ledger.component.html index 963d479dc..def4089c6 100644 --- a/frontend/src/app/features/dashboard/dashboard-ledger/dashboard-ledger.component.html +++ b/frontend/src/app/features/dashboard/dashboard-ledger/dashboard-ledger.component.html @@ -1,6 +1,6 @@
-
Ledgers
+
Ledgers
{{ progress }}
@@ -20,20 +20,8 @@
-
-
-
Snarked - Fetch hashes
-
{{ ledgers.stakingEpoch.snarked.fetchHashesDuration | secDuration: configMap.stakingEpoch }}
-
-
-
Snarked - Fetch accounts
-
{{ ledgers.stakingEpoch.snarked.fetchAccountsDuration | secDuration: configMap.stakingEpoch }}
-
-
+
@@ -46,64 +34,69 @@
-
-
-
- Snarked - Fetch hashes + +
+
+
+ + Snarked ledger at the root +
+
+ {{ rootSnarkedProgress | number: '1.0-0' }}%
-
{{ ledgers.nextEpoch.snarked.fetchHashesDuration | secDuration: configMap.nextEpoch }}
-
-
-
Snarked - Fetch accounts
-
{{ ledgers.nextEpoch.snarked.fetchAccountsDuration | secDuration: configMap.nextEpoch }}
+
+ [ngClass]="ledgers.rootStaged.state">
- - Ledger at the root + + Staged ledger at the root
-
- {{ rootProgress | number: '1.0-0' }}% +
+ {{ rootStagedProgress | number: '1.0-0' }}%
-
Snarked - Fetch hashes
-
{{ ledgers.root.snarked.fetchHashesDuration | secDuration: configMap.root }}
+
Fetch parts
+
{{ ledgers.rootStaged.staged.fetchPartsDuration ?? ledgers.rootStaged.staged.fetchPassedTime | secDuration: configMap.rootStaged }}
-
Snarked - Fetch accounts
-
{{ ledgers.root.snarked.fetchAccountsDuration | secDuration: configMap.root }}
-
-
-
Staged - Fetch parts
-
{{ ledgers.root.staged.fetchPartsDuration | secDuration: configMap.root }}
-
-
-
Staged - Reconstruct
-
{{ ledgers.root.staged.reconstructDuration | secDuration: configMap.root }}
+
Reconstruct
+
{{ ledgers.rootStaged.staged.reconstructDuration ?? ledgers.rootStaged.staged.reconstructPassedTime | secDuration: configMap.rootStaged }}
+ +
+
+
Fetch hashes
+
{{ snarked.fetchHashesDuration ?? snarked.fetchHashesPassedTime | secDuration: config }}
+
+
+
Fetch accounts
+
{{ snarked.fetchAccountsDuration ?? snarked.fetchAccountsPassedTime | secDuration: config }}
+
+
+
+ @@ -122,14 +115,6 @@ - - - - {{ time | secDuration: configMap.stakingEpoch }} - - - -