diff --git a/frontend/angular.json b/frontend/angular.json index a9fea5b127..65e682a597 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -90,6 +90,15 @@ ], "outputHashing": "all" }, + "webnodelocal": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.webnodelocal.ts" + } + ], + "outputHashing": "all" + }, "fuzzing": { "fileReplacements": [ { @@ -120,6 +129,9 @@ "development": { "browserTarget": "frontend:build:development" }, + "webnodelocal": { + "browserTarget": "frontend:build:webnodelocal" + }, "local": { "browserTarget": "frontend:build:local" }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 21897e6c26..631c6daa4d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "1.0.129", + "version": "1.0.183", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "1.0.129", + "version": "1.0.183", "dependencies": { "@angular/animations": "^17.3.12", "@angular/cdk": "^17.3.10", diff --git a/frontend/package.json b/frontend/package.json index 3949213c4b..04b7ce9904 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,10 +1,11 @@ { "name": "frontend", - "version": "1.0.180", + "version": "1.0.183", "scripts": { "install:deps": "npm install", "start": "npm install && ng serve --configuration local --open", "start:dev": "ng serve --configuration development", + "start:webnode": "ng serve --configuration webnodelocal", "start:dev:mobile": "ng serve --configuration development --host 0.0.0.0", "start:fuzzing": "npm run install:deps && ng build --configuration fuzzing && npm run start:bundle", "build": "ng build", diff --git a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html index c38242ee36..33d8678d51 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.html @@ -1,3 +1,5 @@ - - Applications Close Soon arrow_right_alt + + sports_score + Mina Web Node Testnet Finished + sports_score diff --git a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss index 97e0746e11..1efc41ef2d 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss +++ b/frontend/src/app/features/leaderboard/leaderboard-apply/leaderboard-apply.component.scss @@ -24,6 +24,6 @@ } &:hover .mina-icon { - animation: bounceLeftToRight .85s infinite ease-out; + //animation: bounceLeftToRight .85s infinite ease-out; } } diff --git a/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html index e9c9c7bcb1..9e091faf5a 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-header/leaderboard-header.component.html @@ -6,7 +6,7 @@ [@dropdownAnimation]="isMenuOpen ? 'open' : 'closed'" [class.open]="isMenuOpen" (clickOutside)="closeMenu()"> - Leaderboard + Prize results Program Details Prize Draw & Tie-Break Process diff --git a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html index cdb1d68b39..8e8803b91d 100644 --- a/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html +++ b/frontend/src/app/features/leaderboard/leaderboard-page/leaderboard-page.component.html @@ -3,7 +3,7 @@
- +
diff --git a/frontend/src/app/features/network/splits/dashboard-splits-graph/dashboard-splits-graph.component.ts b/frontend/src/app/features/network/splits/dashboard-splits-graph/dashboard-splits-graph.component.ts index 1f290c00dc..de6709bb66 100644 --- a/frontend/src/app/features/network/splits/dashboard-splits-graph/dashboard-splits-graph.component.ts +++ b/frontend/src/app/features/network/splits/dashboard-splits-graph/dashboard-splits-graph.component.ts @@ -43,6 +43,8 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On private tooltip: d3.Selection; private simulation: Simulation; private circles: d3.Selection; + private webnodeInnerCircles: d3.Selection; + private webnodeOuterCircles: d3.Selection; private triangles: d3.Selection; private squares: d3.Selection; private diamonds: d3.Selection; @@ -158,7 +160,8 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On console.log('Spectral Gap:', spectralGap); this.renderGraph({ peers, links, sets }); - }, tap(({ fetching }) => { + }, + tap(({ fetching }) => { if (fetching) { this.fetching = fetching; } @@ -245,7 +248,8 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On .attr('class', d => `link ${d.source.address} ${d.target.address}`) .attr('stroke', 'var(--base-divider)'); - const circleNodes = nodes.filter(peer => !peer.node || ['node', 'generator'].some(n => peer.node.toLowerCase().includes(n))); + const circleNodes = nodes.filter(peer => !peer.node || ['node', 'generator'].some(n => peer.node.toLowerCase().includes(n) && !peer.node.toLowerCase().includes('webnode'))); + const webnodeNodes = nodes.filter(p => p.node).filter(peer => peer.node.toLowerCase().includes('webnode')); const triangleNodes = nodes.filter(p => p.node).filter(peer => peer.node.toLowerCase().includes('snark')); const squareNodes = nodes.filter(p => p.node).filter(peer => peer.node.toLowerCase().includes('prod')); const diamondNodes = nodes.filter(p => p.node).filter(peer => peer.node.toLowerCase().includes('seed')); @@ -259,6 +263,34 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On .attr('r', (d: DashboardSplitsPeerSimulation) => d.radius); this.addCommonProperties(this.circles, lines); + this.webnodeOuterCircles = this.getG('webnodes-outer', 'circle') + .attr('class', 'webnodes-outer') + .selectAll('circle') + .data(webnodeNodes) + .enter() + .append('circle') + .attr('r', (d: DashboardSplitsPeerSimulation) => d.radius) + .attr('fill', 'none') + this.addCommonProperties(this.webnodeOuterCircles, lines, true); + + this.webnodeInnerCircles = this.getG('webnodes-inner', 'circle') + .attr('class', 'webnodes-inner') + .selectAll('circle') + .data(webnodeNodes) + .enter() + .append('circle') + .attr('r', (d: DashboardSplitsPeerSimulation) => d.radius * 0.7) + .attr('fill', 'var(--success-primary)') + .on('mouseover', (event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => this.mouseOverHandle(peer, event, true)) + .on('mouseout', (event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => this.mouseOutHandler(peer, event, true)) + .on('click', (_event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => { + let selectedPeer = this.peers.find(p => p.address === peer.address); + if (selectedPeer === this.activePeer) { + selectedPeer = undefined; + } + this.dispatch(DashboardSplitsSetActivePeer, selectedPeer); + }) + this.triangles = this.getG('triangles', 'path') .attr('class', 'triangles') .selectAll('path') @@ -291,6 +323,30 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On private createSimulation(sets: DashboardSplitsSet[], nodes: DashboardSplitsPeerSimulation[], lines: DashboardSplitsLinkSimulation[]): void { const numberOfSets = sets.length; const matrixSize = Math.ceil(Math.sqrt(numberOfSets)); + const getRepulsion = (sets: number) => { + switch (sets) { + case 1: + return 30; + case 2: + return 25; + case 3: + return 22; + case 4: + return 20; + case 5: + return 15; + case 6: + return 12; + case 7: + return 9; + case 8: + return 7; + case 9: + return 5; + default: + return 2; + } + } this.simulation = d3.forceSimulation(nodes) .force('link', d3.forceLink(lines).distance(numberOfSets === 1 ? 250 : 100)) // This adds links between nodes and sets the distance between them .force('charge', d3.forceManyBody().strength(-30)) // control the repulsion between groups - negative means bigger distance @@ -310,7 +366,7 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On } return this.height; }).strength(0.05)) - .force('collide', d3.forceCollide().radius(numberOfSets < 3 ? 25 : 17)) // This adds repulsion between nodes. Play with the radius + .force('collide', d3.forceCollide().radius(getRepulsion(numberOfSets))) // This adds repulsion between nodes. Play with the radius .force('center', d3.forceCenter(this.width / 2, this.height / 2)) .force('bounds', () => { for (let node of nodes) { @@ -331,6 +387,12 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On this.circles .attr('cx', (d: DashboardSplitsPeerSimulation) => d.x) .attr('cy', (d: DashboardSplitsPeerSimulation) => d.y); + this.webnodeInnerCircles + .attr('cx', (d: DashboardSplitsPeerSimulation) => d.x) + .attr('cy', (d: DashboardSplitsPeerSimulation) => d.y); + this.webnodeOuterCircles + .attr('cx', (d: DashboardSplitsPeerSimulation) => d.x) + .attr('cy', (d: DashboardSplitsPeerSimulation) => d.y); this.triangles .attr('transform', (d: DashboardSplitsPeerSimulation) => `translate(${d.x}, ${d.y})`); this.squares @@ -346,14 +408,14 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On this.simulation.alpha(0.1); // controls how much the animation lasts } - private addCommonProperties(selection: d3.Selection, lines: DashboardSplitsLinkSimulation[]): void { + private addCommonProperties(selection: d3.Selection, lines: DashboardSplitsLinkSimulation[], noColorChange: boolean = false): void { selection .attr('fill', 'var(--special-node)') .attr('stroke', (d: DashboardSplitsPeerSimulation) => `var(--${lines.some(link => link.source.address === d.address || link.target.address === d.address) ? 'success' : 'warn'}-primary)`) .attr('stroke-width', 1) - .on('mouseover', (event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => this.mouseOverHandle(peer, event)) - .on('mouseout', (event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => this.mouseOutHandler(peer, event)) - .on('click', (event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => { + .on('mouseover', (event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => this.mouseOverHandle(peer, event, noColorChange)) + .on('mouseout', (event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => this.mouseOutHandler(peer, event, noColorChange)) + .on('click', (_event: MouseEvent & { target: HTMLElement }, peer: DashboardSplitsPeerSimulation) => { let selectedPeer = this.peers.find(p => p.address === peer.address); if (selectedPeer === this.activePeer) { selectedPeer = undefined; @@ -372,7 +434,7 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On .attr('class', cls); } - private mouseOverHandle(peer: DashboardSplitsPeerSimulation, event: MouseEvent & { target: HTMLElement }): void { + private mouseOverHandle(peer: DashboardSplitsPeerSimulation, event: MouseEvent & { target: HTMLElement }, noColorChange: boolean = false): void { const selection = this.tooltip.html(`${peer.node || peer.address}, ${peer.incomingConnections} / ${peer.outgoingConnections} `) .style('display', 'block'); @@ -386,7 +448,9 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On if (this.activePeer?.address === peer.address) { return; } - d3.select(event.target).attr('fill', 'var(--special-node-selected)'); + if (!noColorChange) { + d3.select(event.target).attr('fill', 'var(--special-node-selected)'); + } this.hoveredConnectedLinks = this.connections.filter(link => { const isDirectConnection = link.source.address === peer.address || link.target.address === peer.address; if (this.activePeer) { @@ -398,12 +462,14 @@ export class DashboardSplitsGraphComponent extends StoreDispatcher implements On this.hoveredConnectedLinks.attr('stroke', 'var(--success-primary)'); } - private mouseOutHandler(peer: DashboardSplitsPeerSimulation, event: MouseEvent & { target: HTMLElement }): void { + private mouseOutHandler(peer: DashboardSplitsPeerSimulation, event: MouseEvent & { target: HTMLElement }, noColorChange: boolean = false): void { this.tooltip.style('display', 'none'); if (this.activePeer?.address === peer.address) { return; } - d3.select(event.target).attr('fill', 'var(--special-node)'); + if (!noColorChange) { + d3.select(event.target).attr('fill', 'var(--special-node)'); + } this.hoveredConnectedLinks.attr('stroke', 'var(--base-divider)'); } } diff --git a/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.html b/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.html index 40eab500d8..33296db549 100644 --- a/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.html +++ b/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.html @@ -5,9 +5,6 @@ {{ row.node || '-' }} - - - {{ row.incomingConnections }} / {{ row.outgoingConnections }} diff --git a/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.scss b/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.scss index e69de29bb2..fbc87b4c17 100644 --- a/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.scss +++ b/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.scss @@ -0,0 +1,5 @@ +:host ::ng-deep .mina-table .row.head { + span:last-child { + justify-content: flex-end; + } +} diff --git a/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.ts b/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.ts index f532cfedfb..7bddead9fb 100644 --- a/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.ts +++ b/frontend/src/app/features/network/splits/dashboard-splits-side-panel-table/dashboard-splits-side-panel-table.component.ts @@ -21,7 +21,6 @@ export class DashboardSplitsSidePanelTableComponent extends MinaTableRustWrapper protected readonly tableHeads: TableColumnList = [ { name: 'address' }, { name: 'name', sort: 'node' }, - { name: 'peer ID', sort: 'peerId' }, { name: 'Conn. \nIn / Out', sort: 'outgoingConnections' }, ]; @@ -30,13 +29,13 @@ export class DashboardSplitsSidePanelTableComponent extends MinaTableRustWrapper constructor(private router: Router) { super(); } override async ngOnInit(): Promise { - this.height = isDesktop() ? ((this.peers.length + 1) * 36 +1) : ((this.peers.length) * 104) + 90; + this.height = isDesktop() ? ((this.peers.length + 1) * 36 + 1) : ((this.peers.length) * 104) + 90; await super.ngOnInit(); this.listenToActivePeerChanges(); } protected override setupTable(): void { - this.table.gridTemplateColumns = [115, 85, 80, '1fr']; + this.table.gridTemplateColumns = [140, 85, '1fr']; this.table.minWidth = 400; this.table.sortClz = DashboardSplitsSortPeers; this.table.sortSelector = selectDashboardSplitsSort; diff --git a/frontend/src/app/features/network/splits/dashboard-splits-toolbar/dashboard-splits-toolbar.component.html b/frontend/src/app/features/network/splits/dashboard-splits-toolbar/dashboard-splits-toolbar.component.html index 3abc7e34b2..a70be8a491 100644 --- a/frontend/src/app/features/network/splits/dashboard-splits-toolbar/dashboard-splits-toolbar.component.html +++ b/frontend/src/app/features/network/splits/dashboard-splits-toolbar/dashboard-splits-toolbar.component.html @@ -3,7 +3,9 @@
-
{{ setsLength === undefined ? 'Loading ' : setsLength }} Branch{{ setsLength | plural: 'es' }}
+
{{ setsLength === undefined ? 'Loading ' : setsLength }} + Branch{{ setsLength | plural: 'es' }} +
- crop_square - {{ stats.producers }} -  Producer{{ stats.producers | plural }} - crop_square - {{ stats.seeders }} -  Seeder{{ stats.seeders | plural }} - change_history - {{ stats.snarkers }} -  Snarker{{ stats.snarkers | plural }} + radio_button_checked +  Current node + + + + + + circle {{ stats.nodes + stats.transactionGenerators }} -  Other +  Peers
diff --git a/frontend/src/app/features/network/splits/dashboard-splits.effects.ts b/frontend/src/app/features/network/splits/dashboard-splits.effects.ts index ad1ec98a51..52c4e332f1 100644 --- a/frontend/src/app/features/network/splits/dashboard-splits.effects.ts +++ b/frontend/src/app/features/network/splits/dashboard-splits.effects.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { Effect } from '@openmina/shared'; -import { EMPTY, map, switchMap } from 'rxjs'; +import { EMPTY, map, switchMap, tap } from 'rxjs'; import { MinaRustBaseEffect } from '@shared/base-classes/mina-rust-base.effect'; import { DASHBOARD_SPLITS_CLOSE, @@ -14,7 +14,7 @@ import { DashboardSplitsActions, DashboardSplitsClose, DashboardSplitsGetSplits, - DashboardSplitsMergeNodes, + DashboardSplitsMergeNodes, DashboardSplitsMergeNodesSuccess, DashboardSplitsSplitNodesSuccess, } from '@network/splits/dashboard-splits.actions'; import { DashboardSplitsService } from '@network/splits/dashboard-splits.service'; import { MinaState, selectMinaState } from '@app/app.setup'; @@ -39,8 +39,8 @@ export class DashboardSplitsEffects extends MinaRustBaseEffect this.actions$.pipe( - ofType(DASHBOARD_SPLITS_GET_SPLITS, DASHBOARD_SPLITS_CLOSE), - this.latestActionState(), + ofType(DASHBOARD_SPLITS_GET_SPLITS, DASHBOARD_SPLITS_SPLIT_NODES_SUCCESS, DASHBOARD_SPLITS_MERGE_NODES_SUCCESS, DASHBOARD_SPLITS_CLOSE), + this.latestActionState(), switchMap(({ action }) => action.type === DASHBOARD_SPLITS_CLOSE ? EMPTY @@ -53,7 +53,7 @@ export class DashboardSplitsEffects extends MinaRustBaseEffect this.actions$.pipe( ofType(DASHBOARD_SPLITS_SPLIT_NODES), this.latestStateSlice('network.splits'), - switchMap(state => this.splitService.splitNodes(state.peers)), + tap(state => this.splitService.splitNodes(state.peers)), map(() => ({ type: DASHBOARD_SPLITS_SPLIT_NODES_SUCCESS })), catchErrorAndRepeat(MinaErrorType.RUST, DASHBOARD_SPLITS_SPLIT_NODES_SUCCESS), )); @@ -61,7 +61,7 @@ export class DashboardSplitsEffects extends MinaRustBaseEffect this.actions$.pipe( ofType(DASHBOARD_SPLITS_MERGE_NODES), this.latestStateSlice('network.splits'), - switchMap(state => this.splitService.mergeNodes(state.peers)), + tap(state => this.splitService.mergeNodes(state.peers)), map(() => ({ type: DASHBOARD_SPLITS_MERGE_NODES_SUCCESS })), catchErrorAndRepeat(MinaErrorType.RUST, DASHBOARD_SPLITS_MERGE_NODES_SUCCESS), )); diff --git a/frontend/src/app/features/network/splits/dashboard-splits.reducer.ts b/frontend/src/app/features/network/splits/dashboard-splits.reducer.ts index 7b76a7c681..1a5d84966a 100644 --- a/frontend/src/app/features/network/splits/dashboard-splits.reducer.ts +++ b/frontend/src/app/features/network/splits/dashboard-splits.reducer.ts @@ -46,7 +46,7 @@ export function topologyReducer(state: DashboardSplitsState = initialState, acti case DASHBOARD_SPLITS_GET_SPLITS_SUCCESS: { const peers = action.payload.peers.map((p) => ({ ...p, - radius: getRadius(p.address, action.payload.links), + radius: getRadius(p, action.payload.links), outgoingConnections: action.payload.links.filter(l => l.source === p.address).length, incomingConnections: action.payload.links.filter(l => l.target === p.address).length, })); @@ -72,6 +72,7 @@ export function topologyReducer(state: DashboardSplitsState = initialState, acti case DASHBOARD_SPLITS_SPLIT_NODES: { return { ...state, + fetching: true, networkSplitsDetails: 'Last split: ' + toReadableDate(Date.now(), noMillisFormat), }; } @@ -79,6 +80,7 @@ export function topologyReducer(state: DashboardSplitsState = initialState, acti case DASHBOARD_SPLITS_MERGE_NODES: { return { ...state, + fetching: true, networkMergeDetails: 'Last merge: ' + toReadableDate(Date.now(), noMillisFormat), }; } @@ -110,8 +112,11 @@ export function topologyReducer(state: DashboardSplitsState = initialState, acti } -function getRadius(address: string, links: DashboardSplitsLink[]): number { - const occurrence = links.filter(link => link.source === address || link.target === address).length; +function getRadius(peer: DashboardSplitsPeer, links: DashboardSplitsLink[]): number { + if (peer.node === 'Webnode') { + return 14; + } + const occurrence = links.filter(link => link.source === peer.address || link.target === peer.address).length; if (occurrence < 6) { return 4; } else if (occurrence < 8) { @@ -176,5 +181,5 @@ function splitThePeers(peers: DashboardSplitsPeer[], links: DashboardSplitsLink[ } function sortPeers(messages: DashboardSplitsPeer[], tableSort: TableSort): DashboardSplitsPeer[] { - return sort(messages, tableSort, ['address', 'peerId', 'node'], true); + return sort(messages, tableSort, ['address', 'node'], true); } diff --git a/frontend/src/app/features/network/splits/dashboard-splits.service.ts b/frontend/src/app/features/network/splits/dashboard-splits.service.ts index 92b32552b0..07bf502fd1 100644 --- a/frontend/src/app/features/network/splits/dashboard-splits.service.ts +++ b/frontend/src/app/features/network/splits/dashboard-splits.service.ts @@ -1,63 +1,23 @@ import { Injectable } from '@angular/core'; -import { concatAll, forkJoin, from, map, Observable, toArray } from 'rxjs'; +import { concatAll, delay, forkJoin, from, map, Observable, tap, toArray } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { DashboardSplits } from '@shared/types/network/splits/dashboard-splits.type'; import { CONFIG } from '@shared/constants/config'; import { DashboardSplitsPeer } from '@shared/types/network/splits/dashboard-splits-peer.type'; import { DashboardSplitsLink } from '@shared/types/network/splits/dashboard-splits-link.type'; -import { peersMock } from '@network/splits/mock'; @Injectable({ providedIn: 'root' }) export class DashboardSplitsService { private readonly options = { headers: { 'Content-Type': 'application/json' } }; + private currentData: DashboardSplits; + constructor(private http: HttpClient) { } getPeers(): Observable { return from( - [ - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node1', - // 'name': 'node1', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node2', - // 'name': 'node2', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node3', - // 'name': 'node3', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node4', - // 'name': 'node4', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node5', - // 'name': 'node5', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node6', - // 'name': 'node6', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node7', - // 'name': 'node7', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node8', - // 'name': 'node8', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node9', - // 'name': 'node9', - // }, - // { - // 'graphql': 'http://1.k8.openmina.com:31754/node10', - // 'name': 'node10', - // }, - ].map(node => + [].map(node => this.http .post<{ data: GetPeersResponse }>(`${node.graphql}/graphql`, { query: peersQuery }, this.options) .pipe( @@ -65,59 +25,382 @@ export class DashboardSplitsService { ), ), ).pipe( + delay(100), concatAll(), toArray(), - map(() => peersMock), - map((array/*: { data: GetPeersResponse, node: string }[]*/) => { - const map = new Map(); - array.forEach(({ data, node }: { data: GetPeersResponse, node: string }) => { - node = node.charAt(0).toUpperCase() + node.slice(1); - node = node.replace(/(\d+)/g, ' $1'); - map.set(data.daemonStatus.addrsAndPorts.externalIp, node); - }); - return array.reduce((acc: DashboardSplits, { data }: { data: GetPeersResponse }) => { - (acc as any).peers.push( - ...[ - ...data.getPeers - .slice(0, 10) - .map((p: GetPeers) => ({ address: p.host, /*peerId: p.peerId,*/ node: map.get(p.host) })), - { - address: data.daemonStatus.addrsAndPorts.externalIp, - // peerId: data.daemonStatus.addrsAndPorts.peer.peerId, - node: map.get(data.daemonStatus.addrsAndPorts.externalIp), - }, - { - address: 'rand', - node: 'seed16', - }, - { - address: 'rand2', - node: 'seed15', - }, - ], - ); - acc.links.push( - ...data.getPeers - .slice(0, 10) - .map((p: GetPeers) => ({ - source: data.daemonStatus.addrsAndPorts.externalIp, - target: p.host, - })), - ); - acc.links.push( - { - source: 'rand2', - target: 'rand', + /* This is how the data was coming from the backend */ + // map(() => peersMock), + // map((array/*: { data: GetPeersResponse, node: string }[]*/) => { + // const map = new Map(); + // array.forEach(({ data, node }: { data: GetPeersResponse, node: string }) => { + // node = node.charAt(0).toUpperCase() + node.slice(1); + // node = node.replace(/(\d+)/g, ' $1'); + // map.set(data.daemonStatus.addrsAndPorts.externalIp, node); + // }); + // return array.reduce((acc: DashboardSplits, { data }: { data: GetPeersResponse }) => { + // (acc as any).peers.push( + // ...[ + // ...data.getPeers + // .slice(0, 10) + // .map((p: GetPeers) => ({ address: p.host, /*peerId: p.peerId,*/ node: map.get(p.host) })), + // { + // address: data.daemonStatus.addrsAndPorts.externalIp, + // // peerId: data.daemonStatus.addrsAndPorts.peer.peerId, + // node: map.get(data.daemonStatus.addrsAndPorts.externalIp), + // }, + // { + // address: 'rand', + // node: 'seed16', + // }, + // { + // address: 'rand2', + // node: 'seed15', + // }, + // ], + // ); + // acc.links.push( + // ...data.getPeers + // .slice(0, 10) + // .map((p: GetPeers) => ({ + // source: data.daemonStatus.addrsAndPorts.externalIp, + // target: p.host, + // })), + // ); + // acc.links.push( + // { + // source: 'rand2', + // target: 'rand', + // } + // ) + // return acc; + // }, { peers: new Array(), links: new Array() }); + // }), + // map((response: DashboardSplits) => this.removeDuplicatedPeers(response)), + map(() => { + // Helper function to generate random IP addresses + function generateRandomIpAddress(): string { + const octet1 = Math.floor(Math.random() * 256); + const octet2 = Math.floor(Math.random() * 256); + const octet3 = Math.floor(Math.random() * 256); + const octet4 = Math.floor(Math.random() * 256); + + return `${octet1}.${octet2}.${octet3}.${octet4}`; + } + + /** + * Generates a network of peers with random connections + * @param peerCount The number of peers to create (default: 10) + * @param connectionsPerPeer The number of random connections per peer (default: 5) + * @returns A DashboardSplits object containing peers and their connections + */ + function generatePeerNetwork(peerCount: number = 100, connectionsPerPeer: number = 50): DashboardSplits { + // Create array to hold our peers + const peers: DashboardSplitsPeer[] = []; + + // Generate peers with random addresses and names + for (let i = 0; i < peerCount; i++) { + const address = generateRandomIpAddress(); + const nodeName = i === 0 ? 'Webnode' : `Node-${i + 1}`; + peers.push({ address, node: nodeName }); + } + + // Create array to hold our links + const links: DashboardSplitsLink[] = []; + + // Create a set to track unique connections and avoid duplicates + const connectionSet = new Set(); + + // For each peer, create random connections + peers.forEach(peer => { + // We want to create exactly connectionsPerPeer links if possible + let connectionsMade = 0; + let attempts = 0; + const maxAttempts = peerCount * 2; // Prevent infinite loops + + while (connectionsMade < connectionsPerPeer && attempts < maxAttempts) { + attempts++; + + // Choose a random target peer that is not the current peer + const randomIndex = Math.floor(Math.random() * peerCount); + const targetPeer = peers[randomIndex]; + + if (targetPeer.address === peer.address) { + continue; // Skip self-connections + } + + // Create a unique identifier for this connection (ordered by address to avoid duplicates) + const sourceAddr = peer.address; + const targetAddr = targetPeer.address; + + // Sort addresses to make sure we don't add the same connection twice in different directions + const [addr1, addr2] = [sourceAddr, targetAddr].sort(); + const connectionKey = `${addr1}-${addr2}`; + + // Check if this connection already exists + if (!connectionSet.has(connectionKey)) { + // Add the connection + links.push({ + source: sourceAddr, + target: targetAddr + }); + + // Mark this connection as used + connectionSet.add(connectionKey); + connectionsMade++; + } } - ) - return acc; - }, { peers: new Array(), links: new Array() }); + }); + + return { + peers, + links + }; + } + + if (this.currentData) { + return { ...this.currentData }; + } + + this.currentData = generatePeerNetwork(); + return this.currentData; }), - map((response: DashboardSplits) => this.removeDuplicatedPeers(response)), + tap((d) => console.log(d)) + ); + } + + splitNodes(_peers?: DashboardSplitsPeer[]): void { + // First, identify the existing disconnected components in the network + const components = this.identifyDisconnectedComponents(); + + // Sort components by size (largest first) to make sure we split the largest groups + components.sort((a, b) => b.length - a.length); + + // Find a group large enough to split (minimum 4 peers to create two groups of at least 2) + const groupToSplitIndex = components.findIndex(group => group.length >= 4); + + // If no group is large enough to split, return the current data unchanged + if (groupToSplitIndex === -1) { + console.log('No groups large enough to split further.'); + this.currentData = { + peers: [...this.currentData.peers], + links: [...this.currentData.links] + }; + } + + // Get the group we'll split + const groupToSplit = components[groupToSplitIndex]; + + if (!groupToSplit) { + return; + } + + // Determine split point - approximately half + const splitIndex = Math.floor(groupToSplit.length / 2); + + // Separate the group into two subgroups + const subgroup1 = groupToSplit.slice(0, splitIndex); + const subgroup2 = groupToSplit.slice(splitIndex); + + // Create sets of addresses for faster lookups + const subgroup1Addresses = new Set(subgroup1); + const subgroup2Addresses = new Set(subgroup2); + + // Filter out links that connect between the two subgroups + const newLinks = this.currentData.links.filter(link => { + // If both endpoints are in the group we're splitting... + if (groupToSplit.includes(link.source) && groupToSplit.includes(link.target)) { + // Keep only if both are in the same subgroup + return ( + (subgroup1Addresses.has(link.source) && subgroup1Addresses.has(link.target)) || + (subgroup2Addresses.has(link.source) && subgroup2Addresses.has(link.target)) + ); + } + // Keep all links not related to the group we're splitting + return true; + }); + + // Ensure each subgroup is connected (has sufficient internal links) + const additionalLinks = [ + ...this.ensureGroupIsConnected(subgroup1, this.currentData.peers), + ...this.ensureGroupIsConnected(subgroup2, this.currentData.peers) + ]; + + // Create a new object with the updated links + this.currentData = { + peers: [...this.currentData.peers], + links: [...newLinks, ...additionalLinks] + }; + } + + /** + * Identifies disconnected components (clusters) in the network + * @returns Array of arrays, where each inner array contains the addresses of peers in one connected component + */ + private identifyDisconnectedComponents(): string[][] { + // Create an adjacency map + const adjacencyMap = new Map>(); + + // Initialize the map with all peers + this.currentData.peers.forEach(peer => { + adjacencyMap.set(peer.address, new Set()); + }); + + // Populate the adjacency map + this.currentData.links.forEach(link => { + adjacencyMap.get(link.source)?.add(link.target); + adjacencyMap.get(link.target)?.add(link.source); + }); + + // Set to track visited nodes + const visited = new Set(); + + // Array to hold the components + const components: string[][] = []; + + // Function to perform depth-first search + const dfs = (node: string, component: string[]) => { + visited.add(node); + component.push(node); + + const neighbors = adjacencyMap.get(node) || new Set(); + for (const neighbor of neighbors) { + if (!visited.has(neighbor)) { + dfs(neighbor, component); + } + } + }; + + // Find all components + for (const peer of this.currentData.peers) { + if (!visited.has(peer.address)) { + const component: string[] = []; + dfs(peer.address, component); + components.push(component); + } + } + + return components; + } + + /** + * Ensures a group of peers is connected by adding links if necessary + * @param group Array of peer addresses in the group + * @param allPeers All peers in the network + * @returns Array of new links needed to ensure connectivity + */ + private ensureGroupIsConnected(group: string[], allPeers: DashboardSplitsPeer[]): DashboardSplitsLink[] { + // If the group has fewer than 2 peers, it can't be connected + if (group.length < 2) { + return []; + } + + // Create a set of addresses for faster lookups + const groupAddresses = new Set(group); + + // Find existing links within this group + const groupLinks = this.currentData.links.filter(link => + groupAddresses.has(link.source) && groupAddresses.has(link.target) ); + + // If there are already links, we need to check if the group is connected + if (groupLinks.length > 0) { + // Use a simplified connectivity check - make sure there's a path to every node + const connectivity = this.checkConnectivity(group, groupLinks); + + if (connectivity.fullyConnected) { + return []; // Already connected, no new links needed + } + + // If not connected, we need to add links between disconnected subgroups + const newLinks: DashboardSplitsLink[] = []; + + // We'll connect each isolated component to the largest component + for (let i = 1; i < connectivity.components.length; i++) { + const sourceNode = connectivity.components[0][0]; // Node from largest component + const targetNode = connectivity.components[i][0]; // Node from isolated component + + newLinks.push({ + source: sourceNode, + target: targetNode + }); + } + + return newLinks; + } else { + // No existing links in this group - create a minimal spanning tree + const newLinks: DashboardSplitsLink[] = []; + + // Simple approach: connect each node to the next one in sequence + for (let i = 0; i < group.length - 1; i++) { + newLinks.push({ + source: group[i], + target: group[i + 1] + }); + } + + return newLinks; + } + } + + /** + * Checks the connectivity of a group of peers + * @param group Array of peer addresses in the group + * @param groupLinks Existing links within the group + * @returns Object containing connectivity information + */ + private checkConnectivity(group: string[], groupLinks: DashboardSplitsLink[]): { + fullyConnected: boolean; + components: string[][] + } { + // Create an adjacency map + const adjacencyMap = new Map>(); + + // Initialize the map with all peers in the group + group.forEach(address => { + adjacencyMap.set(address, new Set()); + }); + + // Populate the adjacency map + groupLinks.forEach(link => { + adjacencyMap.get(link.source)?.add(link.target); + adjacencyMap.get(link.target)?.add(link.source); + }); + + // Set to track visited nodes + const visited = new Set(); + + // Array to hold the components + const components: string[][] = []; + + // Function to perform depth-first search + const dfs = (node: string, component: string[]) => { + visited.add(node); + component.push(node); + + const neighbors = adjacencyMap.get(node) || new Set(); + for (const neighbor of neighbors) { + if (!visited.has(neighbor)) { + dfs(neighbor, component); + } + } + }; + + // Find all components + for (const address of group) { + if (!visited.has(address)) { + const component: string[] = []; + dfs(address, component); + components.push(component); + } + } + + // The group is fully connected if there's only one component + const fullyConnected = components.length === 1; + + return { fullyConnected, components }; } - splitNodes(peers: DashboardSplitsPeer[]): Observable { + splitNodesOld(peers: DashboardSplitsPeer[]): Observable { const nodeName = (peer: DashboardSplitsPeer) => peer.node.toLowerCase().replace(' ', ''); const lastChar = (str: string) => str[str.length - 1]; const leftList = peers.filter(p => p.node).filter(p => Number(lastChar(p.node)) % 2 === 0); @@ -151,7 +434,45 @@ export class DashboardSplitsService { return forkJoin([leftObs, rightObs]).pipe(map(() => void 0)); } - mergeNodes(peers: DashboardSplitsPeer[]): Observable { + mergeNodes(_: any): void { + // Identify the existing disconnected components in the network + const components = this.identifyDisconnectedComponents(); + + // If there's only one component, the network is already fully connected + if (components.length <= 1) { + console.log('Network is already fully connected.'); + this.currentData = { + peers: [...this.currentData.peers], + links: [...this.currentData.links] + }; + } + + // Start with the existing links + const newLinks = [...this.currentData.links]; + + // Connect each component to the next one to form a "chain" of components + for (let i = 0; i < components.length - 1; i++) { + // Get a random node from the current component + const sourceNode = components[i][Math.floor(Math.random() * components[i].length)]; + + // Get a random node from the next component + const targetNode = components[i + 1][Math.floor(Math.random() * components[i + 1].length)]; + + // Add a link between them + newLinks.push({ + source: sourceNode, + target: targetNode + }); + } + + // Create a new object with the updated links + this.currentData = { + peers: [...this.currentData.peers], + links: newLinks + }; + } + + mergeNodesOld(peers: DashboardSplitsPeer[]): Observable { const nodeName = (peer: DashboardSplitsPeer) => peer.node.toLowerCase().replace(' ', ''); return from( diff --git a/frontend/src/app/features/network/splits/mock.ts b/frontend/src/app/features/network/splits/mock.ts index f67fa3610a..56346319fd 100644 --- a/frontend/src/app/features/network/splits/mock.ts +++ b/frontend/src/app/features/network/splits/mock.ts @@ -85,5 +85,29 @@ export const peersMock: any = [ ] }, "node": "node4" + }, + { + "data": { + "daemonStatus": { + "addrsAndPorts": { + "externalIp": "10.233.110.50", + } + }, + "getPeers": [ + { + "host": "10.233.64.200", + }, + { + "host": "10.233.127.9", + }, + { + "host": "10.233.80.87", + }, + { + "host": "10.233.92.19", + }, + ] + }, + "node": "webnode" } ] diff --git a/frontend/src/app/shared/types/network/splits/dashboard-splits-peer.type.ts b/frontend/src/app/shared/types/network/splits/dashboard-splits-peer.type.ts index 83ba01ce03..936ef5062d 100644 --- a/frontend/src/app/shared/types/network/splits/dashboard-splits-peer.type.ts +++ b/frontend/src/app/shared/types/network/splits/dashboard-splits-peer.type.ts @@ -1,6 +1,5 @@ export class DashboardSplitsPeer { address: string; - peerId: string; node: string; radius?: number; outgoingConnections?: number; diff --git a/frontend/src/environments/environment.webnodelocal.ts b/frontend/src/environments/environment.webnodelocal.ts new file mode 100644 index 0000000000..f38213de36 --- /dev/null +++ b/frontend/src/environments/environment.webnodelocal.ts @@ -0,0 +1,61 @@ +import { MinaEnv } from '@shared/types/core/environment/mina-env.type'; + +export const environment: Readonly = { + production: true, + identifier: 'Web Node FE', + canAddNodes: true, + showWebNodeLandingPage: false, + showLeaderboard: false, + hidePeersPill: true, + hideTxPill: true, + globalConfig: { + features: { + dashboard: [], + // nodes: ['overview', 'live', 'bootstrap'], + state: ['actions'], + // network: ['messages', 'connections', 'blocks', 'topology', 'node-dht', 'graph-overview', 'bootstrap-stats'], + // snarks: ['scan-state', 'work-pool'], + // resources: ['memory'], + 'block-production': ['won-slots'], + mempool: [], + benchmarks: ['wallets'], + // fuzzing: [], + }, + firebase: { + apiKey: 'AIzaSyBZzFsHjIbQVbBP0N-KkUsEvHRVU_wwd7g', + authDomain: 'webnode-gtm-test.firebaseapp.com', + projectId: 'webnode-gtm-test', + storageBucket: 'webnode-gtm-test.firebasestorage.app', + messagingSenderId: '1016673359357', + appId: '1:1016673359357:web:bbd2cbf3f031756aec7594', + measurementId: 'G-ENDBL923XT', + }, + heartbeats: true, + graphQL: 'https://adonagy.com/graphql', + // graphQL: 'https://api.minascan.io/node/devnet/v1/graphql', + // graphQL: 'http://65.109.105.40:5000/graphql', + }, + configs: [ + // { + // name: 'staging-devnet-bp-0', + // url: 'https://staging-devnet-openmina-bp-0.minaprotocol.network', + // }, + // { + // name: 'staging-devnet-bp-1', + // url: 'https://staging-devnet-openmina-bp-1.minaprotocol.network', + // }, + // { + // name: 'staging-devnet-bp-2', + // url: 'https://staging-devnet-openmina-bp-2.minaprotocol.network', + // }, + // { + // name: 'staging-devnet-bp-3', + // url: 'https://staging-devnet-openmina-bp-3.minaprotocol.network', + // }, + { + name: 'Web Node', + isWebNode: true, + }, + ], +}; + diff --git a/frontend/src/index.html b/frontend/src/index.html index 65e3742455..007e1d08c1 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -49,11 +49,11 @@