diff --git a/src/app/pages/base-page/base-page.component.ts b/src/app/pages/base-page/base-page.component.ts index 32bb679..6b21ffe 100644 --- a/src/app/pages/base-page/base-page.component.ts +++ b/src/app/pages/base-page/base-page.component.ts @@ -16,9 +16,7 @@ export abstract class BasePageComponent implements AfterContentInit, OnDestroy { protected subscriptions: Subscription[] = []; - constructor(private navbarService: NavbarService) { - - } + constructor(private navbarService: NavbarService) { } protected setLinks(links: NavbarLink[] = []): void { this._links = links; @@ -113,6 +111,62 @@ export abstract class BasePageComponent implements AfterContentInit, OnDestroy { return false; } + protected getTable(id: string): JQuery | undefined { + const initalized: JQuery | undefined = this.initializedTables[id]; + + return initalized; + } + + protected getTableSelection(id: string): TRow[] { + const table = this.getTable(id); + + if (!table) { + return []; + } + + const result = table.bootstrapTable('getSelections') as TRow[]; + + if (!result) { + return []; + } + + return result; + } + + protected removeTableSelection(id: string, key: string): TRow[] { + const table = this.getTable(id); + + if (!table || key === '') { + return []; + } + + const selection = this.getTableSelection(id); + + if (selection.length === 0) { + return []; + } + + const firstRow = selection[0]; + + if (!firstRow[key]) { + console.warn("Cannote remove selection by key: " + key); + return []; + } + + const ids: any[] = []; + + selection.forEach((s) => { + ids.push(s[key]); + }); + + table.bootstrapTable('remove', { + field: key, + values: ids + }); + + return selection; + } + public ngAfterContentInit(): void { this.navbarService.setLinks(this._links); } diff --git a/src/app/pages/settings/settings.component.html b/src/app/pages/settings/settings.component.html index 7c3caab..15d1fea 100644 --- a/src/app/pages/settings/settings.component.html +++ b/src/app/pages/settings/settings.component.html @@ -590,6 +590,100 @@ 18080 for mainnet, 28080 for testnet, 38080 for stagenet + +
+ +

Seed Node

+ +
+ + + Connect to a node to retrieve other nodes' addresses, and disconnect +
+ +
+ + + Specify node port +
+ +
+ +

Exclusive Nodes

+ +
+ +

+ + + + + + + + +
IDAddressPort
+
+ +
+ + + Specify a node to connect to only +
+ +
+ + + Specify node port +
+ +
+ + +
+ +
+ +

Priority Nodes

+ +
+ +

+ + + + + + + + +
IDAddressPort
+
+ +
+ + + Specify a node to connect to only +
+ +
+ + + Specify node port +
+ +
+ + +
diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts index 89bc253..993c48b 100644 --- a/src/app/pages/settings/settings.component.ts +++ b/src/app/pages/settings/settings.component.ts @@ -1,39 +1,32 @@ -import { Component, NgZone } from '@angular/core'; +import { AfterViewInit, Component, NgZone } from '@angular/core'; import { NavbarLink } from '../../shared/components/navbar/navbar.model'; import { DaemonSettings } from '../../../common/DaemonSettings'; import { DaemonService } from '../../core/services/daemon/daemon.service'; import { ElectronService } from '../../core/services'; import { DaemonSettingsError } from '../../../common'; +import { BasePageComponent } from '../base-page/base-page.component'; +import { NavbarService } from '../../shared/components'; @Component({ selector: 'app-settings', templateUrl: './settings.component.html', styleUrl: './settings.component.scss' }) -export class SettingsComponent { +export class SettingsComponent extends BasePageComponent implements AfterViewInit { public readonly navbarLinks: NavbarLink[]; + private removingExclusiveNodes: boolean = false; + private removingPriorityNodes: boolean = false; + + private addingExclusiveNode: boolean = false; + private addingPriorityNode: boolean = false; private originalSettings: DaemonSettings; public currentSettings: DaemonSettings; public banListUrl: string = ''; public remoteBanList: boolean = false; - public get validBanListUrl(): boolean { - if (this.banListUrl == '') { - return false; - } - - try { - new URL(this.banListUrl); - return true; - } - catch { - return false; - } - } - public savingChanges: boolean = false; public savingChangesError = ``; public savingChangesSuccess: boolean = false; @@ -49,12 +42,150 @@ export class SettingsComponent { public databaseSyncMode: 'sync' | 'async' = 'async'; public databaseSyncNBytesOrBlocks: number = 250000000; public databaseSyncNPerMode: 'bytes' | 'blocks' = 'bytes'; + public isPortable: boolean = true; + public downloadingBanListFile: boolean = false; + + public exclusiveNodeAddress: string = ''; + public exclusiveNodePort: number = 0; + + public priorityNodeAddress: string = ''; + public priorityNodePort: number = 0; + + public seedNodeAddress: string = ''; + public seedNodePort: number = 0; + + private get seedNode(): string { + return `${this.seedNodeAddress}:${this.seedNodePort}`; + } + + private get exclusiveNodes(): NodeInfo[] { + const result: { address: string, port: number }[] = []; + + this.currentSettings.exclusiveNodes.forEach((en) => { + const components = en.split(":"); + + if (components.length !== 2) { + return; + } + + const [ address, strPort ] = components; + + try { + const port = parseInt(strPort); + + result.push({ address: address, port: port }); + } + catch { + return; + } + + }); + + return result; + } + + private get priorityNodes(): NodeInfo[] { + const result: { address: string, port: number }[] = []; + + this.currentSettings.priorityNodes.forEach((en) => { + const components = en.split(":"); + + if (components.length !== 2) { + return; + } + + const [ address, strPort ] = components; + + try { + const port = parseInt(strPort); + + result.push({ address: address, port: port }); + } + catch { + return; + } + + }); + + return result; + } private get dbSyncMode(): string { return `${this.databaseSyncSpeed}:${this.databaseSyncMode}:${this.databaseSyncNBytesOrBlocks}${this.databaseSyncNPerMode}`; } - constructor(private daemonService: DaemonService, private electronService: ElectronService, private ngZone: NgZone) { + public get validBanListUrl(): boolean { + if (this.banListUrl == '') { + return false; + } + + try { + new URL(this.banListUrl); + return true; + } + catch { + return false; + } + } + + public get canAddExclusiveNode(): boolean { + if (this.addingExclusiveNode) { + return false; + } + + if (this.exclusiveNodePort <= 0) { + return false; + } + + if (this.exclusiveNodeAddress.trim().replace(' ', '') === '' || this.exclusiveNodeAddress.includes(":")) { + return false; + } + + if (this.exclusiveNodes.find((en) => en.address === this.exclusiveNodeAddress && en.port === this.exclusiveNodePort)) { + return false; + } + + return true; + } + + public get canRemoveExclusiveNodes(): boolean { + if (this.removingExclusiveNodes || this.exclusiveNodes.length === 0) { + return false; + } + + return true; + } + + public get canRemovePriorityNodes(): boolean { + if (this.removingPriorityNodes || this.priorityNodes.length === 0) { + return false; + } + + return true; + } + + public get canAddPriorityNode(): boolean { + if (this.addingPriorityNode) { + return false; + } + + if (this.priorityNodePort <= 0) { + return false; + } + + if (this.priorityNodeAddress.trim().replace(' ', '') === '' || this.priorityNodeAddress.includes(":")) { + return false; + } + + if (this.priorityNodes.find((en) => en.address === this.priorityNodeAddress && en.port === this.priorityNodePort)) { + return false; + } + + return true; + } + + constructor(private daemonService: DaemonService, private electronService: ElectronService, navbarService: NavbarService, private ngZone: NgZone) { + super(navbarService); this.loading = true; this.navbarLinks = [ @@ -86,7 +217,9 @@ export class SettingsComponent { }); } - public isPortable: boolean = true; + public ngAfterViewInit(): void { + this.loadTables(); + } public refreshSyncMode(): void { setTimeout(() => { @@ -125,6 +258,22 @@ export class SettingsComponent { private async load(): Promise { this.originalSettings = await this.daemonService.getSettings(); this.currentSettings = this.originalSettings.clone(); + + if (this.seedNode !== '') { + const components = this.seedNode.split(":"); + + if (components.length >= 2) { + const [node, strPort] = components; + + this.seedNodeAddress = node; + this.seedNodePort = parseInt(strPort); + } + else if (components.length === 1) + { + this.seedNodeAddress = components[0]; + } + } + this.initSyncMode(); this.loading = false; @@ -133,6 +282,23 @@ export class SettingsComponent { this.networkType = this.currentSettings.mainnet ? 'mainnet' : this.currentSettings.testnet ? 'testnet' : this.currentSettings.stagenet ? 'stagenet' : 'mainnet'; } + private loadTables(): void { + setTimeout(() => { + this.ngZone.run(() => { + this.loadExclusiveNodesTable(); + this.loadPriorityNodesTable(); + }); + }, 500); + } + + private loadExclusiveNodesTable(loading: boolean = false): void { + this.loadTable('exclusiveNodesTable', this.exclusiveNodes, loading); + } + + private loadPriorityNodesTable(loading: boolean = false): void { + this.loadTable('priorityNodesTable', this.priorityNodes, loading); + } + public get modified(): boolean { if (!this.currentSettings.equals(this.originalSettings)) { return true; @@ -145,6 +311,15 @@ export class SettingsComponent { return !this.modified || this.daemonService.restarting || this.daemonService.starting || this.daemonService.stopping; } + public onSeedNodeChange() { + if (this.seedNodePort >= 0) { + this.currentSettings.seedNode = `${this.seedNodeAddress}:${this.seedNodePort}`; + } + else { + this.currentSettings.seedNode = this.seedNodeAddress; + } + } + public OnOfflineChange() { this.currentSettings.offline = !this.currentSettings.offline; } @@ -384,8 +559,6 @@ export class SettingsComponent { this.currentSettings.banList = ''; } - public downloadingBanListFile: boolean = false; - public async downloadBanListFile(): Promise { if (!this.remoteBanList) { return; @@ -535,4 +708,85 @@ export class SettingsComponent { public removeLogFile(): void { this.currentSettings.logFile = ''; } + + public addExclusiveNode(): void { + if (!this.canAddExclusiveNode) { + return; + } + + this.addingExclusiveNode = true; + + try { + const node = `${this.exclusiveNodeAddress}:${this.exclusiveNodePort}`; + + this.currentSettings.addExclusiveNode(node); + + this.loadExclusiveNodesTable(); + } + catch (error: any) { + console.error(error); + } + + this.addingExclusiveNode = false; + } + + public removeSelectedExclusiveNodes(): void { + if (!this.canRemoveExclusiveNodes) { + return; + } + + this.removingExclusiveNodes = true; + + const removed = this.removeTableSelection('exclusiveNodesTable', 'address'); + + removed.forEach((r) => { + const node = r.port >= 0 ? `${r.address}:${r.port}` : r.address; + this.currentSettings.removeExclusiveNode(node); + }); + + this.loadExclusiveNodesTable(); + + this.removingExclusiveNodes = false; + } + + public addPriorityNode(): void { + if (!this.canAddPriorityNode) { + return; + } + + this.addingPriorityNode = true; + + try { + const node = `${this.priorityNodeAddress}:${this.priorityNodePort}`; + + this.currentSettings.addPriorityNode(node); + this.loadPriorityNodesTable(); + } + catch (error: any) { + console.error(error); + } + + this.addingPriorityNode = false; + } + + public removeSelectedPriorityNodes(): void { + if (!this.canRemovePriorityNodes) { + return; + } + + this.removingPriorityNodes = true; + + const removed = this.removeTableSelection('priorityNodesTable', 'address'); + + removed.forEach((r) => { + const node = r.port >= 0 ? `${r.address}:${r.port}` : r.address; + this.currentSettings.removePriorityNode(node); + }); + + this.loadPriorityNodesTable(); + + this.removingPriorityNodes = false; + } } + +interface NodeInfo { address: string, port: number }; \ No newline at end of file diff --git a/src/common/DaemonSettings.ts b/src/common/DaemonSettings.ts index 2c213c3..4a1df50 100644 --- a/src/common/DaemonSettings.ts +++ b/src/common/DaemonSettings.ts @@ -94,7 +94,7 @@ export class DaemonSettings { public addPeer: string = ''; //public addPriorityNode: string = ''; //public addExclusiveNode: string = ''; - public exlusiveNodes: string[] = []; + public exclusiveNodes: string[] = []; public priorityNodes: string[] = []; public seedNode: string = ''; @@ -155,7 +155,7 @@ export class DaemonSettings { } public get hasExclusiveNodes(): boolean { - return this.exlusiveNodes.length > 0; + return this.exclusiveNodes.length > 0; } public get hasPriorityNodes(): boolean { @@ -163,15 +163,15 @@ export class DaemonSettings { } public addExclusiveNode(node: string): void { - if (this.exlusiveNodes.includes(node)) { + if (this.exclusiveNodes.includes(node)) { throw new DaemonSettingsDuplicateExclusiveNodeError(node); } - this.exlusiveNodes.push(node); + this.exclusiveNodes.push(node); } public removeExclusiveNode(node: string): void { - this.exlusiveNodes = this.exlusiveNodes.filter((n) => n !== node); + this.exclusiveNodes = this.exclusiveNodes.filter((n) => n !== node); } public addPriorityNode(node: string): void { @@ -179,7 +179,7 @@ export class DaemonSettings { throw new DaemonSettingsDuplicatePriorityNodeError(node); } - this.exlusiveNodes.push(node); + this.priorityNodes.push(node); } public removePriorityNode(node: string): void { @@ -230,15 +230,40 @@ export class DaemonSettings { // Se una chiave di obj1 non esiste in obj2, non sono uguali if (!keys2.includes(key)) return false; + if (key == 'exclusiveNodes' && obj1 instanceof DaemonSettings && obj2 instanceof DaemonSettings) { + if (obj1.exclusiveNodes.length !== obj2.exclusiveNodes.length) return false; + else { + for(const en of obj1.exclusiveNodes) { + if (!obj2.exclusiveNodes.includes(en)) return false; + } + } + } + else if (key == 'priorityNodes' && obj1 instanceof DaemonSettings && obj2 instanceof DaemonSettings) { + if (obj1.priorityNodes.length !== obj2.priorityNodes.length) return false; + else { + for(const en of obj1.priorityNodes) { + if (!obj2.priorityNodes.includes(en)) return false; + } + } + } + else if (!this.deepEqual(obj1[key], obj2[key])) return false; + // Se il valore della proprietà non è uguale, effettua un confronto ricorsivo - if (!this.deepEqual(obj1[key], obj2[key])) return false; } return true; } public clone(): DaemonSettings { - return Object.assign(new DaemonSettings(), this); + const result = Object.assign(new DaemonSettings(), this); + + result.exclusiveNodes = []; + result.priorityNodes = []; + + this.exclusiveNodes.forEach((en) => result.addExclusiveNode(en)); + this.priorityNodes.forEach((pn) => result.addPriorityNode(pn)); + + return result; } public static parse(data: any): DaemonSettings { @@ -473,7 +498,7 @@ export class DaemonSettings { if (this.allowLocalIp) options.push(`--allow-local-ip`); if (this.addPeer != '') options.push('--add-peer', this.addPeer); if (this.hasPriorityNodes) this.priorityNodes.forEach((node) => options.push(`--add-priority-node`, node)); - if (this.hasExclusiveNodes) this.exlusiveNodes.forEach((node) => options.push(`--add-exlcusive-node`, node)); + if (this.hasExclusiveNodes) this.exclusiveNodes.forEach((node) => options.push(`--add-exlcusive-node`, node)); if (this.seedNode != '') options.push(`--seed-node`, this.seedNode); if (this.txProxy != '') options.push(`--tx-proxy`, this.txProxy); if (this.anonymousInbound != '') options.push(`--anonymous-inbound`, this.anonymousInbound);