9 Commits

Author SHA1 Message Date
Stefan
6b4a125846 add simplified install & formatting 2022-11-23 23:29:41 +01:00
Stefan
eb76268fd5 formatting 2022-11-23 23:26:06 +01:00
Stefan
cea055a545 add web3 2022-11-23 23:25:55 +01:00
Stefan
1386443be2 format pubkey func 2022-11-23 23:25:49 +01:00
Stefan
828b105f5f add global mock 2022-11-23 17:59:08 +01:00
Stefan
d2dd72e0e3 add web devserver config 2022-11-23 17:58:07 +01:00
Stefan
93cfaeee5f add testing framework 2022-11-23 17:57:49 +01:00
Stefan
e38f4c87b7 initial dashboard view 2022-08-15 19:27:12 +02:00
Stefan
43e162f998 dashboard layout 2022-08-10 23:49:07 +02:00
28 changed files with 8699 additions and 1503 deletions

3
.gitignore vendored
View File

@@ -8,4 +8,5 @@ dist/
!.yarn/sdks
!.yarn/versions
build/
build/
validator_keys

View File

@@ -5,5 +5,6 @@
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"useTabs": false
"useTabs": false,
"printWidth": 120
}

14
jest.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { Config } from 'jest';
const config: Config = {
verbose: true,
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.ts?$': 'ts-jest',
},
transformIgnorePatterns: ['<rootDir>/node_modules/'],
};
export default config;

View File

@@ -7,24 +7,32 @@
"author": "Colfax Selby <colfax.selby@gmail.com>",
"license": "GPL",
"devDependencies": {
"@types/jest": "^29.2.3",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.1.8",
"css-loader": "^6.7.0",
"electron": "^17.1.0",
"electron-builder": "^22.14.5",
"electron-mock-ipc": "^0.3.12",
"jest": "^29.3.1",
"style-loader": "^3.3.1",
"ts-jest": "^29.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.9.1",
"typescript": "^4.3.5",
"webpack": "^5.64.3",
"webpack-cli": "^4.9.1"
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.11.1"
},
"scripts": {
"build": "webpack --config webpack.react.config.js --config webpack.electron.config.js",
"build:watch": "webpack --config webpack.react.config.js --config webpack.electron.config.js --watch",
"dev": "",
"start": "electron ./build/electron/index.js",
"pack": "electron-builder --dir",
"dist": "electron-builder"
"dist": "electron-builder",
"test": "jest"
},
"dependencies": {
"@emotion/react": "^11.8.1",
@@ -41,7 +49,9 @@
"react-router-dom": "^6.2.2",
"shebang-loader": "^0.0.1",
"sudo-prompt": "^9.2.1",
"tmp-promise": "^3.0.3"
"tmp-promise": "^3.0.3",
"web3": "^1.8.1",
"web3-eth": "^1.8.1"
},
"build": {
"appId": "gg.wagyu.installer",

11
rome.json Normal file
View File

@@ -0,0 +1,11 @@
{
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentSize": 2,
"lineWidth": 90
},
"linter": {
"enabled": false
}
}

View File

@@ -1,16 +1,16 @@
import sudo from 'sudo-prompt';
import sudo from "sudo-prompt";
import { commandJoin } from "command-join";
import { generate as generate_password } from "generate-password";
import { exec, execFile } from 'child_process';
import { promisify } from 'util';
import { withDir } from 'tmp-promise';
import { exec, execFile } from "child_process";
import { promisify } from "util";
import { withDir } from "tmp-promise";
import { open, rm, mkdir } from 'fs/promises';
import { open, rm, mkdir } from "fs/promises";
import path from 'path';
import os from 'os';
import path from "path";
import os from "os";
import {
ExecutionClient,
@@ -19,19 +19,28 @@ import {
NodeStatus,
ValidatorStatus,
InstallDetails,
OutputLogs
OutputLogs,
} from "./IMultiClientInstaller";
import { Network, networkToExecution } from '../react/types';
import { doesFileExist, doesDirectoryExist } from './BashUtils';
import {
Network,
networkToExecution,
BeaconGetSyncingStatus,
BeaconResponse,
BeaconGetPeers,
} from "../react/types";
import { doesFileExist, doesDirectoryExist } from "./BashUtils";
import { nullAddress } from "../react/constants";
import Web3 from "web3";
import { Syncing } from "web3-eth";
const execFileProm = promisify(execFile);
const execProm = promisify(exec);
const dockerServiceName = 'docker.service';
const dockerGroupName = 'docker';
const installPath = path.join(os.homedir(), '.wagyu-installer');
const ethDockerGitRepository = 'https://github.com/eth-educators/eth-docker.git';
const prysmWalletPasswordFileName = 'prysm-wallet-password';
const dockerServiceName = "docker.service";
const dockerGroupName = "docker";
const installPath = path.join(os.homedir(), ".wagyu-installer");
const ethDockerGitRepository = "https://github.com/eth-educators/eth-docker.git";
const prysmWalletPasswordFileName = "prysm-wallet-password";
type SystemdServiceDetails = {
description: string | undefined;
@@ -39,68 +48,120 @@ type SystemdServiceDetails = {
activeState: string | undefined;
subState: string | undefined;
unitFileState: string | undefined;
}
};
const CLPROVIDER = "http://127.0.0.1:5052";
const ELPROVIDER = "ws://127.0.0.1:8546";
const writeOutput = (message: string, outputLogs?: OutputLogs): void => {
const web3 = new Web3(ELPROVIDER);
const writeOutput = (
message: string,
progress?: number,
outputLogs?: OutputLogs,
): void => {
if (outputLogs) {
outputLogs(message);
if (progress) {
outputLogs("INFO: " + message, progress);
} else {
outputLogs("INFO: " + message);
}
}
};
export class EthDockerInstaller implements IMultiClientInstaller {
title = 'Electron';
title = "Electron";
async preInstall(outputLogs?: OutputLogs): Promise<boolean> {
let packagesToInstall = new Array<string>();
let progress = 0;
// We need git installed
const gitPackageName = 'git';
const gitPackageName = "git";
writeOutput(`Checking if ${gitPackageName} is installed.`, outputLogs);
const gitInstalled = await this.checkForInstalledUbuntuPackage(gitPackageName);
writeOutput(`Checking if ${gitPackageName} is installed.`, ++progress, outputLogs);
const gitInstalled = await this.checkForLinuxBinary(gitPackageName);
if (!gitInstalled) {
writeOutput(`${gitPackageName} is not installed. We will install it.`, outputLogs);
writeOutput(
`${gitPackageName} is not installed. We will install it.`,
++progress,
outputLogs,
);
packagesToInstall.push(gitPackageName);
} else {
writeOutput(`${gitPackageName} is already installed. We will not install it.`, outputLogs);
writeOutput(
`${gitPackageName} is already installed. We will not install it.`,
++progress,
outputLogs,
);
}
// We need docker installed, enabled and running
const dockerPackageName = 'docker-compose';
let needToEnableDockerService = true;
const dockerPackageName = "docker-compose";
// let needToEnableDockerService = true;
let needToEnableDockerService = false;
let needToStartDockerService = false;
let needToCheckDockerService = false;
writeOutput(`Checking if ${dockerPackageName} is installed.`, outputLogs);
const dockerInstalled = await this.checkForInstalledUbuntuPackage(dockerPackageName);
writeOutput(`Checking if ${dockerPackageName} is installed.`, ++progress, outputLogs);
const dockerInstalled = await this.checkForLinuxBinary(dockerPackageName);
if (!dockerInstalled) {
writeOutput(`${dockerPackageName} is not installed. We will install it.`, outputLogs);
writeOutput(
`${dockerPackageName} is not installed. We will install it.`,
++progress,
outputLogs,
);
packagesToInstall.push(dockerPackageName);
} else {
writeOutput(`${dockerPackageName} is already installed. We will not install it.`, outputLogs);
writeOutput(
`${dockerPackageName} is already installed. We will not install it.`,
++progress,
outputLogs,
);
writeOutput(`Checking if we need to enable or start the ${dockerServiceName} service.`, outputLogs);
const dockerServiceDetails = await this.getSystemdServiceDetails(dockerServiceName);
needToEnableDockerService = dockerServiceDetails.unitFileState != 'enabled';
if (needToEnableDockerService) {
writeOutput(`The ${dockerServiceName} service is not enabled. We will enable it.`, outputLogs);
}
needToStartDockerService = dockerServiceDetails.subState != 'running';
if (needToStartDockerService) {
writeOutput(`The ${dockerServiceName} service is not started. We will start it.`, outputLogs);
if (needToCheckDockerService) {
writeOutput(
`Checking if we need to enable or start the ${dockerServiceName} service.`,
++progress,
outputLogs,
);
const dockerServiceDetails = await this.getSystemdServiceDetails(
dockerServiceName,
);
needToEnableDockerService = dockerServiceDetails.unitFileState != "enabled";
if (needToEnableDockerService) {
writeOutput(
`The ${dockerServiceName} service is not enabled. We will enable it.`,
++progress,
outputLogs,
);
}
needToStartDockerService = dockerServiceDetails.subState != "running";
if (needToStartDockerService) {
writeOutput(
`The ${dockerServiceName} service is not started. We will start it.`,
++progress,
outputLogs,
);
}
}
}
// We need our user to be in docker group
writeOutput(`Checking if current user is in ${dockerGroupName} group.`, outputLogs);
const needUserInDockerGroup = !await this.isUserInGroup(dockerGroupName);
writeOutput(
`Checking if current user is in ${dockerGroupName} group.`,
++progress,
outputLogs,
);
const needUserInDockerGroup = !(await this.isUserInGroup(dockerGroupName));
if (needUserInDockerGroup) {
writeOutput(`Current user is not in ${dockerGroupName} group. We will add it.`, outputLogs);
writeOutput(
`Current user is not in ${dockerGroupName} group. We will add it.`,
++progress,
outputLogs,
);
}
// We need our installPath directory
writeOutput(`Creating install directory in ${installPath}.`, outputLogs);
writeOutput(`Creating install directory in ${installPath}.`, ++progress, outputLogs);
await mkdir(installPath, { recursive: true });
return await this.preInstallAdminScript(
@@ -108,7 +169,8 @@ export class EthDockerInstaller implements IMultiClientInstaller {
needUserInDockerGroup,
needToEnableDockerService,
needToStartDockerService,
outputLogs);
outputLogs,
);
}
async getSystemdServiceDetails(serviceName: string): Promise<SystemdServiceDetails> {
@@ -117,15 +179,24 @@ export class EthDockerInstaller implements IMultiClientInstaller {
loadState: undefined,
activeState: undefined,
subState: undefined,
unitFileState: undefined
unitFileState: undefined,
};
const properties = ['Description', 'LoadState', 'ActiveState', 'SubState', 'UnitFileState'];
const properties = [
"Description",
"LoadState",
"ActiveState",
"SubState",
"UnitFileState",
];
const { stdout, stderr } = await execFileProm('systemctl',
['show', serviceName, '--property=' + properties.join(',')]);
const { stdout, stderr } = await execFileProm("systemctl", [
"show",
serviceName,
"--property=" + properties.join(","),
]);
const lines = stdout.split('\n');
const lines = stdout.split("\n");
const lineRegex = /(?<key>[^=]+)=(?<value>.*)/;
for (const line of lines) {
const found = line.match(lineRegex);
@@ -161,8 +232,8 @@ export class EthDockerInstaller implements IMultiClientInstaller {
needUserInDockerGroup: boolean,
needToEnableDockerService: boolean,
needToStartDockerService: boolean,
outputLogs?: OutputLogs): Promise<boolean> {
outputLogs?: OutputLogs,
): Promise<boolean> {
if (
packagesToInstall.length > 0 ||
needUserInDockerGroup ||
@@ -174,18 +245,17 @@ export class EthDockerInstaller implements IMultiClientInstaller {
let commandResult = false;
await withDir(async dirResult => {
await withDir(async (dirResult) => {
const scriptPath = path.join(dirResult.path, "commands.sh");
let scriptContent = "";
const scriptPath = path.join(dirResult.path, 'commands.sh');
let scriptContent = '';
const scriptFile = await open(scriptPath, 'w');
await scriptFile.write('#!/bin/bash\n');
const scriptFile = await open(scriptPath, "w");
await scriptFile.write("#!/bin/bash\n");
// Install APT packages
if (packagesToInstall.length > 0) {
const aptUpdate = 'apt -y update\n';
const aptInstall = 'apt -y install ' + commandJoin(packagesToInstall) + '\n';
const aptUpdate = "apt -y update\n";
const aptInstall = "apt -y install " + commandJoin(packagesToInstall) + "\n";
scriptContent += aptUpdate + aptInstall;
@@ -195,7 +265,8 @@ export class EthDockerInstaller implements IMultiClientInstaller {
// Enable docker service
if (needToEnableDockerService) {
const systemctlEnable = 'systemctl enable --now ' + commandJoin([dockerServiceName]) + '\n';
const systemctlEnable =
"systemctl enable --now " + commandJoin([dockerServiceName]) + "\n";
scriptContent += systemctlEnable;
@@ -204,7 +275,8 @@ export class EthDockerInstaller implements IMultiClientInstaller {
// Start docker service
if (needToStartDockerService) {
const systemctlStart = 'systemctl start ' + commandJoin([dockerServiceName]) + '\n';
const systemctlStart =
"systemctl start " + commandJoin([dockerServiceName]) + "\n";
scriptContent += systemctlStart;
@@ -224,45 +296,58 @@ export class EthDockerInstaller implements IMultiClientInstaller {
await scriptFile.chmod(0o500);
await scriptFile.close();
writeOutput(`Running script ${scriptPath} with the following content as root:\n${scriptContent}`, outputLogs);
writeOutput(
`Running script ${scriptPath} with the following content as root:\n${scriptContent}`,
50,
outputLogs,
);
const promise = new Promise<boolean>(async (resolve, reject) => {
const options = {
name: this.title
name: this.title,
};
try {
sudo.exec(scriptPath, options,
function (error, stdout, stderr) {
if (error) reject(error);
if (stdout) {
writeOutput(stdout.toString(), outputLogs);
}
resolve(true);
sudo.exec(scriptPath, options, function (error, stdout, stderr) {
console.log("std err", stderr);
if (error) reject(error);
if (stdout) {
writeOutput(stdout.toString(), 80, outputLogs);
}
);
resolve(true);
});
} catch (error) {
console.log("error executing script", error);
resolve(false);
}
});
await promise.then(result => {
commandResult = result;
}).catch(reason => {
commandResult = false;
}).finally(async () => {
await rm(scriptPath);
});
await promise
.then((result) => {
commandResult = result;
})
.catch((reason) => {
commandResult = false;
console.log("command errored", reason);
})
.finally(async () => {
await rm(scriptPath);
});
});
writeOutput(
`Finished pre-install ${commandResult ? "successfully" : "unsuccessfully"}`,
100,
outputLogs,
);
return commandResult;
} else {
writeOutput(`All dependencies are already installed`, 100, outputLogs);
return true;
}
}
async getUsername(): Promise<string> {
const { stdout, stderr } = await execFileProm('whoami');
const { stdout, stderr } = await execFileProm("whoami");
const userName = stdout.trim();
return userName;
@@ -271,143 +356,187 @@ export class EthDockerInstaller implements IMultiClientInstaller {
async isUserInGroup(groupName: string): Promise<boolean> {
const userName = await this.getUsername();
const { stdout, stderr } = await execFileProm('groups', [userName]);
const groups = stdout.trim().split(' ');
return groups.findIndex(val => val === groupName) >= 0;
const { stdout, stderr } = await execFileProm("groups", [userName]);
const groups = stdout.trim().split(" ");
return groups.findIndex((val) => val === groupName) >= 0;
}
async checkForInstalledUbuntuPackage(packageName: string): Promise<boolean> {
const { stdout, stderr } = await execFileProm('apt', ['-qq', 'list', packageName]);
return stdout.indexOf('[installed]') > 0
const { stdout, stderr } = await execFileProm("apt", [
"-qq",
"list",
"--installed",
packageName,
]);
return stdout.indexOf("installed") > 0;
}
async checkForLinuxBinary(binary: string): Promise<boolean> {
const { stdout, stderr } = await execFileProm("which", [binary]);
return stdout.indexOf(binary) > 0;
}
async install(details: InstallDetails, outputLogs?: OutputLogs): Promise<boolean> {
// Install and update eth-docker
if (!await this.installUpdateEthDockerCode(details.network)) {
writeOutput(`installing ethdocker`, 5, outputLogs);
if (!(await this.installUpdateEthDockerCode(details.network, outputLogs))) {
return false;
}
writeOutput(`installing environment variables`, 33, outputLogs);
// Create .env file with all the configuration details
if (!await this.createEthDockerEnvFile(details)) {
if (!(await this.createEthDockerEnvFile(details, outputLogs))) {
return false;
}
writeOutput(`installing clients`, 66, outputLogs);
// Build the clients
if (!await this.buildClients(details.network)) {
if (!(await this.buildClients(details.network, outputLogs))) {
return false;
}
writeOutput(`installing clients`, 100, outputLogs);
return true;
}
async buildClients(network: Network): Promise<boolean> {
async buildClients(network: Network, outputLogs?: OutputLogs): Promise<boolean> {
const networkPath = path.join(installPath, network.toLowerCase());
const ethDockerPath = path.join(networkPath, 'eth-docker');
const ethDockerPath = path.join(networkPath, "eth-docker");
const ethdCommand = path.join(ethDockerPath, 'ethd');
const ethdCommand = path.join(ethDockerPath, "ethd");
const bashScript = `
/usr/bin/newgrp ${dockerGroupName} <<EONG
${ethdCommand} cmd build --pull
EONG
`;
const returnProm = execProm(bashScript, { shell: '/bin/bash', cwd: ethDockerPath });
const returnProm = execProm(bashScript, { shell: "/bin/bash", cwd: ethDockerPath });
const { stdout, stderr } = await returnProm;
console.log("building clients", stdout, "err: ", stderr);
writeOutput(`client containers available`, 80, outputLogs);
if (returnProm.child.exitCode !== 0) {
console.log('We failed to build eth-docker clients.');
console.log("We failed to build eth-docker clients.");
return false;
}
return true;
}
async createEthDockerEnvFile(details: InstallDetails): Promise<boolean> {
async createEthDockerEnvFile(
details: InstallDetails,
outputLogs?: OutputLogs,
): Promise<boolean> {
const networkPath = path.join(installPath, details.network.toLowerCase());
const ethDockerPath = path.join(networkPath, 'eth-docker');
const ethDockerPath = path.join(networkPath, "eth-docker");
// Start with the default env file.
const defaultEnvPath = path.join(ethDockerPath, 'default.env');
const envPath = path.join(ethDockerPath, '.env');
const defaultEnvPath = path.join(ethDockerPath, "default.env");
const envPath = path.join(ethDockerPath, ".env");
// Open default env file and update the configs.
const defaultEnvFile = await open(defaultEnvPath, 'r');
const defaultEnvConfigs = await defaultEnvFile.readFile({ encoding: 'utf8' });
const defaultEnvFile = await open(defaultEnvPath, "r");
const defaultEnvConfigs = await defaultEnvFile.readFile({ encoding: "utf8" });
await defaultEnvFile.close();
writeOutput(`env file configured`, 50, outputLogs);
let envConfigs = defaultEnvConfigs;
// Writing consensus network
const networkValue = details.network.toLowerCase();
envConfigs = envConfigs.replace(/NETWORK=(.*)/, `NETWORK=${networkValue}`);
// const networkValue = details.network.toLowerCase();
const ecNetworkValue = networkToExecution
.get(details.network)
?.toLowerCase() as string;
envConfigs = envConfigs.replace(/NETWORK=(.*)/, `NETWORK=${ecNetworkValue}`);
// Writing execution network
const ecNetworkValue = networkToExecution.get(details.network)?.toLowerCase() as string;
envConfigs = envConfigs.replace(/EC_NETWORK=(.*)/, `EC_NETWORK=${ecNetworkValue}`);
// envConfigs = envConfigs.replace(/EC_NETWORK=(.*)/, `EC_NETWORK=${ecNetworkValue}`);
// Write fee recipient
envConfigs = envConfigs.replace(/FEE_RECIPIENT=(.*)/, `FEE_RECIPIENT=${nullAddress}`);
envConfigs = envConfigs.replace(/HOST_IP=(.*)/, `127.0.0.1`);
let composeFileValues = new Array<string>();
switch (details.consensusClient) {
case ConsensusClient.LIGHTHOUSE:
composeFileValues.push('lh-base.yml');
composeFileValues.push("lighthouse.yml");
break;
case ConsensusClient.NIMBUS:
composeFileValues.push('nimbus-base.yml');
composeFileValues.push("nimbus.yml");
break;
case ConsensusClient.PRYSM:
composeFileValues.push('prysm-base.yml');
composeFileValues.push("prysm.yml");
break;
case ConsensusClient.TEKU:
composeFileValues.push('teku-base.yml');
composeFileValues.push("teku.yml");
break;
case ConsensusClient.LODESTAR:
composeFileValues.push('lodestar-base.yml');
composeFileValues.push("lodestar.yml");
break;
}
composeFileValues.push("cl-shared.yml");
switch (details.executionClient) {
case ExecutionClient.GETH:
composeFileValues.push('geth.yml');
composeFileValues.push("geth.yml");
break;
case ExecutionClient.NETHERMIND:
composeFileValues.push('nm.yml');
composeFileValues.push("nevermind.yml");
break;
case ExecutionClient.BESU:
composeFileValues.push('besu.yml');
composeFileValues.push("besu.yml");
break;
case ExecutionClient.ERIGON:
composeFileValues.push('erigon.yml');
composeFileValues.push("erigon.yml");
break;
}
composeFileValues.push("el-shared.yml");
const composeFileValue = composeFileValues.join(':');
envConfigs = envConfigs.replace(/COMPOSE_FILE=(.*)/, `COMPOSE_FILE=${composeFileValue}`);
const composeFileValue = composeFileValues.join(":");
envConfigs = envConfigs.replace(
/COMPOSE_FILE=(.*)/,
`COMPOSE_FILE=${composeFileValue}`,
);
// Write our new env file
const envFile = await open(envPath, 'w');
envFile.writeFile(envConfigs, { encoding: 'utf8' });
const envFile = await open(envPath, "w");
envFile.writeFile(envConfigs, { encoding: "utf8" });
await envFile.close();
writeOutput(`env file configured`, 66, outputLogs);
return true;
}
async installUpdateEthDockerCode(network: Network): Promise<boolean> {
async installUpdateEthDockerCode(
network: Network,
outputLogs?: OutputLogs,
): Promise<boolean> {
const networkPath = path.join(installPath, network.toLowerCase());
// Make sure the networkPath is a directory
const networkPathExists = await doesFileExist(networkPath);
const networkPathIsDir = networkPathExists && await doesDirectoryExist(networkPath);
const networkPathIsDir = networkPathExists && (await doesDirectoryExist(networkPath));
if (!networkPathExists) {
await mkdir(networkPath, { recursive: true });
writeOutput(`network path created ${network}`, 5, outputLogs);
} else if (networkPathExists && !networkPathIsDir) {
await rm(networkPath);
await mkdir(networkPath, { recursive: true });
writeOutput(`network path reconfigured ${network}`, 5, outputLogs);
}
const ethDockerPath = path.join(networkPath, 'eth-docker');
const ethDockerPath = path.join(networkPath, "eth-docker");
const ethDockerPathExists = await doesFileExist(ethDockerPath);
const ethDockerPathIsDir = ethDockerPathExists && await doesDirectoryExist(ethDockerPath);
const ethDockerPathIsDir =
ethDockerPathExists && (await doesDirectoryExist(ethDockerPath));
let needToClone = !ethDockerPathExists;
if (ethDockerPathExists && !ethDockerPathIsDir) {
@@ -415,7 +544,9 @@ EONG
needToClone = true;
} else if (ethDockerPathIsDir) {
// Check if eth-docker was already cloned.
const returnProm = execFileProm('git', ['remote', 'show', 'origin'], { cwd: ethDockerPath });
const returnProm = execFileProm("git", ["remote", "show", "origin"], {
cwd: ethDockerPath,
});
const { stdout, stderr } = await returnProm;
if (returnProm.child.exitCode === 0) {
@@ -430,7 +561,7 @@ EONG
needToClone = true;
}
} else {
console.log('Cannot parse `git remote show origin` output.');
console.log("Cannot parse `git remote show origin` output.");
return false;
}
} else {
@@ -442,34 +573,37 @@ EONG
if (needToClone) {
// Clone repository if needed
const returnProm = execFileProm('git', ['clone', ethDockerGitRepository], { cwd: networkPath });
const returnProm = execFileProm("git", ["clone", ethDockerGitRepository], {
cwd: networkPath,
});
const { stdout, stderr } = await returnProm;
if (returnProm.child.exitCode !== 0) {
console.log('We failed to clone eth-docker repository.');
console.log("We failed to clone eth-docker repository.");
return false;
}
// Generate Prysm wallet password and store it in plain text
const walletPassword = generate_password({
length: 32,
numbers: true
numbers: true,
});
const walletPasswordPath = path.join(ethDockerPath, prysmWalletPasswordFileName);
const walletPasswordFile = await open(walletPasswordPath, 'w');
const walletPasswordFile = await open(walletPasswordPath, "w");
await walletPasswordFile.write(walletPassword);
await walletPasswordFile.close();
} else {
// Update repository
const returnProm = execFileProm('git', ['pull'], { cwd: ethDockerPath });
const returnProm = execFileProm("git", ["pull"], { cwd: ethDockerPath });
const { stdout, stderr } = await returnProm;
if (returnProm.child.exitCode !== 0) {
console.log('We failed to update eth-docker repository.');
console.log("We failed to update eth-docker repository.");
return false;
}
}
writeOutput(`ethdocker available`, 25, outputLogs);
return true;
}
@@ -479,20 +613,20 @@ EONG
async stopNodes(network: Network, outputLogs?: OutputLogs): Promise<boolean> {
const networkPath = path.join(installPath, network.toLowerCase());
const ethDockerPath = path.join(networkPath, 'eth-docker');
const ethDockerPath = path.join(networkPath, "eth-docker");
const ethdCommand = path.join(ethDockerPath, 'ethd');
const ethdCommand = path.join(ethDockerPath, "ethd");
const bashScript = `
/usr/bin/newgrp ${dockerGroupName} <<EONG
${ethdCommand} stop
EONG
`;
const returnProm = execProm(bashScript, { shell: '/bin/bash', cwd: ethDockerPath });
const returnProm = execProm(bashScript, { shell: "/bin/bash", cwd: ethDockerPath });
const { stdout, stderr } = await returnProm;
if (returnProm.child.exitCode !== 0) {
console.log('We failed to stop eth-docker clients.');
console.log("We failed to stop eth-docker clients.");
return false;
}
@@ -501,23 +635,25 @@ EONG
async startNodes(network: Network, outputLogs?: OutputLogs): Promise<boolean> {
const networkPath = path.join(installPath, network.toLowerCase());
const ethDockerPath = path.join(networkPath, 'eth-docker');
const ethDockerPath = path.join(networkPath, "eth-docker");
const ethdCommand = path.join(ethDockerPath, 'ethd');
const ethdCommand = path.join(ethDockerPath, "ethd");
const bashScript = `
/usr/bin/newgrp ${dockerGroupName} <<EONG
${ethdCommand} start
${ethdCommand} cmd up -d execution consensus
EONG
`;
const returnProm = execProm(bashScript, { shell: '/bin/bash', cwd: ethDockerPath });
const returnProm = execProm(bashScript, { shell: "/bin/bash", cwd: ethDockerPath });
const { stdout, stderr } = await returnProm;
if (returnProm.child.exitCode !== 0) {
console.log('We failed to start eth-docker clients.');
console.log("We failed to start eth-docker clients.");
return false;
}
writeOutput(`nodes started`, 100, outputLogs);
return true;
}
@@ -537,18 +673,20 @@ EONG
network: Network,
keyStoreDirectoryPath: string,
keyStorePassword: string,
outputLogs?: OutputLogs): Promise<boolean> {
outputLogs?: OutputLogs,
): Promise<boolean> {
const networkPath = path.join(installPath, network.toLowerCase());
const ethDockerPath = path.join(networkPath, 'eth-docker');
const ethDockerPath = path.join(networkPath, "eth-docker");
const ethdCommand = path.join(ethDockerPath, 'ethd');
const ethdCommand = path.join(ethDockerPath, "ethd");
const argKeyStoreDirectoryPath = commandJoin([keyStoreDirectoryPath]);
const argKeyStorePassword = commandJoin([keyStorePassword]);
const walletPasswordPath = path.join(ethDockerPath, prysmWalletPasswordFileName);
const walletPasswordFile = await open(walletPasswordPath, 'r');
const walletPassword = commandJoin([await walletPasswordFile.readFile({ encoding: 'utf8' })]);
const walletPasswordFile = await open(walletPasswordPath, "r");
const walletPassword = commandJoin([
await walletPasswordFile.readFile({ encoding: "utf8" }),
]);
await walletPasswordFile.close();
const bashScript = `
@@ -557,11 +695,13 @@ WALLET_PASSWORD=${walletPassword} KEYSTORE_PASSWORD=${argKeyStorePassword} ${eth
EONG
`;
const returnProm = execProm(bashScript, { shell: '/bin/bash', cwd: ethDockerPath });
const returnProm = execProm(bashScript, { shell: "/bin/bash", cwd: ethDockerPath });
const { stdout, stderr } = await returnProm;
console.log("out: ", stdout);
console.error(stderr);
if (returnProm.child.exitCode !== 0) {
console.log('We failed to import keys with eth-docker.');
console.log("We failed to import keys with eth-docker.");
return false;
}
@@ -591,11 +731,27 @@ EONG
return false;
}
// Data
async getCurrentExecutionClient(): Promise<ExecutionClient> {
// TODO: implement
console.log("Executing getCurrentExecutionClient");
const info = await web3.eth.getNodeInfo();
let gethRex = new RegExp(`${ExecutionClient.GETH.toLowerCase()}`);
if (gethRex.test(info.toLowerCase())) {
return ExecutionClient.GETH;
}
let besuRex = new RegExp(`${ExecutionClient.BESU.toLowerCase()}`);
if (besuRex.test(info.toLowerCase())) {
return ExecutionClient.BESU;
}
let erigonRex = new RegExp(`${ExecutionClient.ERIGON.toLowerCase()}`);
if (erigonRex.test(info.toLowerCase())) {
return ExecutionClient.ERIGON;
}
let nmRex = new RegExp(`${ExecutionClient.NETHERMIND.toLowerCase()}`);
if (nmRex.test(info.toLowerCase())) {
return ExecutionClient.NETHERMIND;
}
return ExecutionClient.GETH;
}
@@ -606,15 +762,14 @@ EONG
}
async getCurrentExecutionClientVersion(): Promise<string> {
// TODO: implement
console.log("Executing getCurrentExecutionClientVersion");
return "0.1";
}
async getCurrentConsensusClientVersion(): Promise<string> {
// TODO: implement
console.log("Executing getCurrentConsensusClientVersion");
return "0.1";
return web3.eth.getNodeInfo();
}
async getLatestExecutionClientVersion(client: ExecutionClient): Promise<string> {
@@ -630,15 +785,46 @@ EONG
}
async executionClientStatus(): Promise<NodeStatus> {
// TODO: implement
console.log("Executing executionClientStatus");
const isSyncing = await web3.eth.isSyncing();
const peerCount = await web3.eth.net.getPeerCount();
if (isSyncing === false && peerCount === 0) {
return NodeStatus.DOWN;
}
if (isSyncing === false && peerCount > 0) {
return NodeStatus.LOOKING_FOR_PEERS;
}
if (
isSyncing === true ||
(isSyncing as Syncing).CurrentBlock + 3 < (isSyncing as Syncing).HighestBlock
) {
return NodeStatus.UP_AND_SYNCING;
}
if ((isSyncing as Syncing).CurrentBlock <= (isSyncing as Syncing).HighestBlock) {
return NodeStatus.UP_AND_SYNCED;
}
return NodeStatus.UNKNOWN;
}
async consensusBeaconNodeStatus(): Promise<NodeStatus> {
// TODO: implement
console.log("Executing consensusBeaconNodeStatus");
return NodeStatus.UNKNOWN;
return fetch(`${CLPROVIDER}/eth/v1/node/syncing`)
.then((res) => res.json())
.then(({ data }: BeaconResponse<BeaconGetSyncingStatus>) => {
if (data.is_syncing) {
return NodeStatus.UP_AND_SYNCING;
} else {
return NodeStatus.UP_AND_SYNCED;
}
})
.catch((err) => {
console.log(err);
return NodeStatus.DOWN;
});
}
async consensusValidatorStatus(): Promise<ValidatorStatus> {
@@ -654,26 +840,34 @@ EONG
}
async executionClientPeerCount(): Promise<number> {
// TODO: implement
console.log("Executing executionClientPeerCount");
return -1;
return web3.eth.net.getPeerCount();
}
async consensusClientPeerCount(): Promise<number> {
// TODO: implement
console.log("Executing consensusClientPeerCount");
return -1;
return fetch(`${CLPROVIDER}/eth/v1/node/peers`)
.then((res) => res.json())
.then(({ meta }: BeaconResponse<BeaconGetPeers>) => {
return meta.count;
})
.catch((err) => {
console.log(err);
return 0;
});
}
async executionClientLatestBlock(): Promise<number> {
// TODO: implement
console.log("Executing executionClientLatestBlock");
return -1;
return web3.eth.getBlockNumber();
}
async consensusClientLatestBlock(): Promise<number> {
// TODO: implement
console.log("Executing consensusClientLatestBlock");
return -1;
return fetch(`${CLPROVIDER}/eth/v1/node/syncing`)
.then((res) => res.json())
.then(({ data }: BeaconResponse<BeaconGetSyncingStatus>) => {
return parseInt(data.head_slot);
})
.catch((err) => {
console.log(err);
return 0;
});
}
}

View File

@@ -1,70 +1,81 @@
import { Network } from "../react/types";
export interface IMultiClientInstaller {
// Functionality
preInstall: (outputLogs?: OutputLogs) => Promise<boolean>,
install: (details: InstallDetails, outputLogs?: OutputLogs) => Promise<boolean>,
postInstall: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>,
preInstall: (outputLogs?: OutputLogs) => Promise<boolean>;
install: (details: InstallDetails, outputLogs?: OutputLogs) => Promise<boolean>;
postInstall: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>;
stopNodes: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>,
startNodes: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>,
stopNodes: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>;
startNodes: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>;
updateExecutionClient: (outputLogs?: OutputLogs) => Promise<void>,
updateConsensusClient: (outputLogs?: OutputLogs) => Promise<void>,
updateExecutionClient: (outputLogs?: OutputLogs) => Promise<void>;
updateConsensusClient: (outputLogs?: OutputLogs) => Promise<void>;
importKeys: (
network: Network,
keyStoreDirectoryPath: string,
keyStorePassword: string,
outputLogs?: OutputLogs) => Promise<boolean>,
exportKeys: () => Promise<void>,
outputLogs?: OutputLogs,
) => Promise<boolean>;
exportKeys: () => Promise<void>;
switchExecutionClient: (targetClient: ExecutionClient, outputLogs?: OutputLogs) => Promise<boolean>,
switchConsensusClient: (targetClient: ConsensusClient, outputLogs?: OutputLogs) => Promise<boolean>,
uninstall: (outputLogs?: OutputLogs) => Promise<boolean>,
switchExecutionClient: (
targetClient: ExecutionClient,
outputLogs?: OutputLogs,
) => Promise<boolean>;
switchConsensusClient: (
targetClient: ConsensusClient,
outputLogs?: OutputLogs,
) => Promise<boolean>;
uninstall: (outputLogs?: OutputLogs) => Promise<boolean>;
// Data
getCurrentExecutionClient: () => Promise<ExecutionClient>,
getCurrentConsensusClient: () => Promise<ConsensusClient>,
getCurrentExecutionClient: () => Promise<ExecutionClient>;
getCurrentConsensusClient: () => Promise<ConsensusClient>;
getCurrentExecutionClientVersion: () => Promise<string>,
getCurrentConsensusClientVersion: () => Promise<string>,
getCurrentExecutionClientVersion: () => Promise<string>;
getCurrentConsensusClientVersion: () => Promise<string>;
getLatestExecutionClientVersion: (client: ExecutionClient) => Promise<string>,
getLatestConsensusClientVersion: (client: ConsensusClient) => Promise<string>,
getLatestExecutionClientVersion: (client: ExecutionClient) => Promise<string>;
getLatestConsensusClientVersion: (client: ConsensusClient) => Promise<string>;
executionClientStatus: () => Promise<NodeStatus>,
consensusBeaconNodeStatus: () => Promise<NodeStatus>,
consensusValidatorStatus: () => Promise<ValidatorStatus>,
executionClientStatus: () => Promise<NodeStatus>;
consensusBeaconNodeStatus: () => Promise<NodeStatus>;
consensusValidatorStatus: () => Promise<ValidatorStatus>;
consensusValidatorCount: () => Promise<number>,
consensusValidatorCount: () => Promise<number>;
executionClientPeerCount: () => Promise<number>,
consensusClientPeerCount: () => Promise<number>,
executionClientPeerCount: () => Promise<number>;
consensusClientPeerCount: () => Promise<number>;
executionClientLatestBlock: () => Promise<number>,
consensusClientLatestBlock: () => Promise<number>,
executionClientLatestBlock: () => Promise<number>;
consensusClientLatestBlock: () => Promise<number>;
// TODO: logs stream
}
export interface OutputLogs {
(message: string): void;
(message: string, progress?: number): void;
}
export interface Progress {
(percent: number): void;
}
export type InstallDetails = {
network: Network,
executionClient: ExecutionClient,
consensusClient: ConsensusClient,
}
debug: boolean;
network: Network;
executionClient: ExecutionClient;
consensusClient: ConsensusClient;
};
export enum NodeStatus {
UP_AND_SYNCED,
UP_AND_SYNCING,
DOWN,
LOOKING_FOR_PEERS,
UNKNOWN,
}
@@ -72,7 +83,7 @@ export enum ValidatorStatus {
UP_AND_VALIDATING,
UP_NOT_VALIDATING,
DOWN,
UNKNOWN
UNKNOWN,
}
// Supported clients
@@ -80,7 +91,7 @@ export enum ExecutionClient {
GETH = "geth",
NETHERMIND = "nethermind",
BESU = "besu",
ERIGON = "erigon"
ERIGON = "erigon",
}
export enum ConsensusClient {
@@ -88,5 +99,5 @@ export enum ConsensusClient {
NIMBUS = "nimbus",
LIGHTHOUSE = "lighthouse",
PRYSM = "prysm",
LODESTAR = "lodestar"
LODESTAR = "lodestar",
}

View File

@@ -9,66 +9,81 @@ import {
clipboard,
ipcRenderer,
OpenDialogOptions,
OpenDialogReturnValue
OpenDialogReturnValue,
} from "electron";
import { Network } from "../react/types";
import { doesDirectoryExist, findFirstFile } from './BashUtils';
import { doesDirectoryExist, findFirstFile } from "./BashUtils";
import { EthDockerInstaller } from './EthDockerInstaller';
import { EthDockerInstaller } from "./EthDockerInstaller";
import { InstallDetails, OutputLogs } from "./IMultiClientInstaller";
import { Writable } from 'stream';
import { Writable } from "stream";
const ethDockerInstaller = new EthDockerInstaller();
const ethDockerPreInstall = async (outputLogs?: OutputLogs): Promise<boolean> => {
return ethDockerInstaller.preInstall(outputLogs);
};
const ethDockerInstall = async (details: InstallDetails): Promise<boolean> => {
return ethDockerInstaller.install(details);
const ethDockerInstall = async (
details: InstallDetails,
outputLogs?: OutputLogs,
): Promise<boolean> => {
return ethDockerInstaller.install(details, outputLogs);
};
const ethDockerImportKeys = async (
network: Network,
keyStoreDirectoryPath: string,
keyStorePassword: string): Promise<boolean> => {
keyStorePassword: string,
): Promise<boolean> => {
return ethDockerInstaller.importKeys(network, keyStoreDirectoryPath, keyStorePassword);
};
const ethDockerPostInstall = async (network: Network): Promise<boolean> => {
return ethDockerInstaller.postInstall(network);
const ethDockerPostInstall = async (
network: Network,
outputLogs?: OutputLogs,
): Promise<boolean> => {
return ethDockerInstaller.postInstall(network, outputLogs);
};
const ethDockerStartNodes = async (network: Network): Promise<boolean> => {
return ethDockerInstaller.startNodes(network);
const ethDockerStartNodes = async (
network: Network,
outputLogs?: OutputLogs,
): Promise<boolean> => {
return ethDockerInstaller.startNodes(network, outputLogs);
};
const ethDockerStopNodes = async (network: Network): Promise<boolean> => {
return ethDockerInstaller.stopNodes(network);
const ethDockerStopNodes = async (
network: Network,
outputLogs?: OutputLogs,
): Promise<boolean> => {
return ethDockerInstaller.stopNodes(network, outputLogs);
};
const ipcRendererSendClose = () => {
ipcRenderer.send('close');
ipcRenderer.send("close");
};
const invokeShowOpenDialog = (options: OpenDialogOptions): Promise<OpenDialogReturnValue> => {
return ipcRenderer.invoke('showOpenDialog', options);
const invokeShowOpenDialog = (
options: OpenDialogOptions,
): Promise<OpenDialogReturnValue> => {
return ipcRenderer.invoke("showOpenDialog", options);
};
contextBridge.exposeInMainWorld('electronAPI', {
'shellOpenExternal': shell.openExternal,
'shellShowItemInFolder': shell.showItemInFolder,
'clipboardWriteText': clipboard.writeText,
'ipcRendererSendClose': ipcRendererSendClose,
'invokeShowOpenDialog': invokeShowOpenDialog
contextBridge.exposeInMainWorld("electronAPI", {
shellOpenExternal: shell.openExternal,
shellShowItemInFolder: shell.showItemInFolder,
clipboardWriteText: clipboard.writeText,
ipcRendererSendClose: ipcRendererSendClose,
invokeShowOpenDialog: invokeShowOpenDialog,
});
contextBridge.exposeInMainWorld('bashUtils', {
'doesDirectoryExist': doesDirectoryExist,
'findFirstFile': findFirstFile
contextBridge.exposeInMainWorld("bashUtils", {
doesDirectoryExist: doesDirectoryExist,
findFirstFile: findFirstFile,
});
contextBridge.exposeInMainWorld('ethDocker', {
'preInstall': ethDockerPreInstall,
'install': ethDockerInstall,
'importKeys': ethDockerImportKeys,
'postInstall': ethDockerPostInstall,
'startNodes': ethDockerStartNodes,
'stopNodes': ethDockerStopNodes
});
contextBridge.exposeInMainWorld("ethDocker", {
preInstall: ethDockerPreInstall,
install: ethDockerInstall,
importKeys: ethDockerImportKeys,
postInstall: ethDockerPostInstall,
startNodes: ethDockerStartNodes,
stopNodes: ethDockerStopNodes,
});

View File

@@ -3,62 +3,51 @@
* This file contains the typescript type hinting for the preload.ts API.
*/
import {
OpenDialogOptions,
OpenDialogReturnValue
} from "electron";
import { OpenDialogOptions, OpenDialogReturnValue } from "electron";
import {
FileOptions,
FileResult
} from "tmp";
import { FileOptions, FileResult } from "tmp";
import {
PathLike,
Stats,
Dirent
} from "fs"
import { PathLike, Stats, Dirent } from "fs";
import {
ChildProcess
} from "child_process"
import { ChildProcess } from "child_process";
import {
EthDockerInstaller
} from './EthDockerInstaller'
import { EthDockerInstaller } from "./EthDockerInstaller";
import { InstallDetails, OutputLogs } from "./IMultiClientInstaller";
export interface IElectronAPI {
shellOpenExternal: (url: string, options?: Electron.OpenExternalOptions | undefined) => Promise<void>,
shellShowItemInFolder: (fullPath: string) => void,
clipboardWriteText: (ext: string, type?: "selection" | "clipboard" | undefined) => void,
ipcRendererSendClose: () => void,
invokeShowOpenDialog: (options: OpenDialogOptions) => Promise<OpenDialogReturnValue>
shellOpenExternal: (
url: string,
options?: Electron.OpenExternalOptions | undefined,
) => Promise<void>;
shellShowItemInFolder: (fullPath: string) => void;
clipboardWriteText: (ext: string, type?: "selection" | "clipboard" | undefined) => void;
ipcRendererSendClose: () => void;
invokeShowOpenDialog: (options: OpenDialogOptions) => Promise<OpenDialogReturnValue>;
}
export interface IBashUtilsAPI {
doesDirectoryExist: (directory: string) => Promise<boolean>,
isDirectoryWritable: (directory: string) => Promise<boolean>,
findFirstFile: (directory: string, startsWith: string) => Promise<string>
doesDirectoryExist: (directory: string) => Promise<boolean>;
isDirectoryWritable: (directory: string) => Promise<boolean>;
findFirstFile: (directory: string, startsWith: string) => Promise<string>;
}
export interface IEthDockerAPI {
preInstall: (outputLogs?: OutputLogs) => Promise<boolean>,
install: (details: InstallDetails) => Promise<boolean>,
preInstall: (outputLogs?: OutputLogs) => Promise<boolean>;
install: (details: InstallDetails, outputLogs?: OutputLogs) => Promise<boolean>;
importKeys: (
network: Network,
keyStoreDirectoryPath: string,
keyStorePassword: string) => Promise<boolean>,
postInstall: (network: Network) => Promise<boolean>,
startNodes: (network: Network) => Promise<boolean>,
stopNodes: (network: Network) => Promise<boolean>,
keyStorePassword: string,
) => Promise<boolean>;
postInstall: (network: Network, outputLogs?: OutputLogs) => Promise<boolean>;
startNodes: (network: Network) => Promise<boolean>;
stopNodes: (network: Network) => Promise<boolean>;
}
declare global {
interface Window {
electronAPI: IElectronAPI,
bashUtils: IBashUtilsAPI,
ethDocker: IEthDockerAPI
electronAPI: IElectronAPI;
bashUtils: IBashUtilsAPI;
ethDocker: IEthDockerAPI;
}
}
}

View File

@@ -0,0 +1,52 @@
import { EthDockerInstaller } from "../EthDockerInstaller";
test("get the right user", async () => {
const installer = new EthDockerInstaller();
const user = await installer.getUsername();
expect(user).toEqual("");
});
describe("preinstall ethdocker", () => {
function output(res: any) {
console.log(res);
}
const installer = new EthDockerInstaller();
beforeAll(async () => {
await installer.preInstall();
});
test("pre-install", async () => {
await installer.preInstall();
});
test("user is in docker group", async () => {
const isInGroup = await installer.isUserInGroup("docker");
expect(isInGroup).toBeTruthy();
});
test("check installed ubuntu package git", async () => {
const isInstalled = await installer.checkForLinuxBinary("git");
expect(isInstalled).toBeTruthy();
});
test("check installed ubuntu package docker", async () => {
const isInstalled = await installer.checkForLinuxBinary("docker");
expect(isInstalled).toBeTruthy();
});
test("check installed ubuntu package docker compose", async () => {
const isInstalled = await installer.checkForLinuxBinary("docker-compose");
expect(isInstalled).toBeTruthy();
});
test("check systemd service details", async () => {
const service = await installer.getSystemdServiceDetails("docker");
expect(service.description).toMatch("Docker Application Container Engine");
expect(service.loadState).toMatch("loaded");
expect(service.activeState).toMatch("active");
expect(service.subState).toMatch("running");
expect(service.unitFileState).toMatch("enabled");
});
});

View File

@@ -1,33 +1,41 @@
import { HashRouter, Route, Routes } from "react-router-dom";
import React, { FC, ReactElement, useState } from "react";
import styled from '@emotion/styled';
import styled from "@emotion/styled";
import Home from "./pages/Home";
import CssBaseline from '@mui/material/CssBaseline';
import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider } from "@mui/material";
import '@fontsource/roboto';
import "@fontsource/roboto";
import MainWizard from "./pages/MainWizard";
import theme from "./theme";
import { Network } from './types';
import { Network } from "./types";
import SystemOverview from "./pages/SystemOverview";
import { ConsensusClient, ExecutionClient, InstallDetails } from "../electron/IMultiClientInstaller";
import {
ConsensusClient,
ExecutionClient,
InstallDetails,
} from "../electron/IMultiClientInstaller";
const Container = styled.main`
display: block;
padding: 20px;
height: inherit;
width: inherit;
padding-bottom: 45px;
`;
// /systemOverview
/**
* The React app top level including theme and routing.
*
*
* @returns the react element containing the app
*/
const App: FC = (): ReactElement => {
// const [network, setNetwork] = useState<Network>(Network.PRATER);
const [installationDetails, setInstallationDetails] = useState<InstallDetails>({
consensusClient: ConsensusClient.PRYSM,
executionClient: ExecutionClient.GETH,
network: Network.PRATER
})
debug: false, // used to test the frontend
consensusClient: ConsensusClient.PRYSM,
executionClient: ExecutionClient.GETH,
network: Network.PRATER,
});
return (
<ThemeProvider theme={theme}>
@@ -35,9 +43,28 @@ const App: FC = (): ReactElement => {
<HashRouter>
<Container>
<Routes>
<Route path="/" element={<Home installationDetails={installationDetails} setInstallationDetails={setInstallationDetails} />} />
<Route path="/wizard/:stepSequenceKey" element={<MainWizard installationDetails={installationDetails} setInstallationDetails={setInstallationDetails} />} />
<Route path="/systemOverview" element={<SystemOverview installationDetails={installationDetails} />} />
<Route
path="/"
element={
<Home
installationDetails={installationDetails}
setInstallationDetails={setInstallationDetails}
/>
}
/>
<Route
path="/wizard/:stepSequenceKey"
element={
<MainWizard
installationDetails={installationDetails}
setInstallationDetails={setInstallationDetails}
/>
}
/>
<Route
path="/dashboard"
element={<SystemOverview installationDetails={installationDetails} />}
/>
</Routes>
</Container>
</HashRouter>

View File

@@ -1,58 +1,75 @@
import { BackgroundLight, } from '../colors';
import { Button, Typography, Box, Grid, Modal, InputAdornment, TextField, styled } from '@mui/material';
import React, { FC, ChangeEvent, Dispatch, ReactElement, SetStateAction, MutableRefObject } from 'react';
import { BackgroundLight } from '../colors';
import {
Button,
Typography,
Box,
Grid,
Modal,
InputAdornment,
TextField,
styled,
// useFormControl,
TextFieldProps,
} from '@mui/material';
import React, {
FC,
ChangeEvent,
Dispatch,
ReactElement,
SetStateAction,
MutableRefObject,
useMemo,
useState,
} from 'react';
import { FileCopy, LockOpen } from '@mui/icons-material'
import { FileCopy, LockOpen } from '@mui/icons-material';
import { InstallDetails } from '../../electron/IMultiClientInstaller';
const ModalStyle = {
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 600,
padding: '20px',
borderRadius: '20px',
background: BackgroundLight,
boxShadow: 24,
p: 4,
};
position: 'absolute' as 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 600,
padding: '20px',
borderRadius: '20px',
background: BackgroundLight,
boxShadow: 24,
p: 4,
};
const FileUploadField = styled(TextField)({
'& label.Mui-focused': {
},
'& .MuiInput-underline:after': {
},
'& .MuiOutlinedInput-root': {
paddingLeft: '0',
const FileUploadField = styled(TextField)({
'& label.Mui-focused': {},
'& .MuiInput-underline:after': {},
'& .MuiOutlinedInput-root': {
paddingLeft: '0',
cursor: 'pointer',
'&:hover': {
cursor: 'pointer',
'&:hover': {
cursor: 'pointer'
},
'&:hover fieldset': {
cursor: 'pointer',
},
'&.Mui-focused fieldset': {
cursor: 'pointer'
},
},
});
'&:hover fieldset': {
cursor: 'pointer',
},
'&.Mui-focused fieldset': {
cursor: 'pointer',
},
},
});
type ImportKeystoreProps = {
setModalOpen: Dispatch<SetStateAction<boolean>>
isModalOpen : boolean
keyStorePath: string
keystorePassword: string
setKeystorePath: Dispatch<SetStateAction<string>>
setKeystorePassword: Dispatch<SetStateAction<string>>
closing: MutableRefObject<((arg: () => Promise<boolean>) => void) | undefined>
installationDetails: InstallDetails
}
setModalOpen: Dispatch<SetStateAction<boolean>>;
isModalOpen: boolean;
keyStorePath: string;
keystorePassword: string;
setKeystorePath: Dispatch<SetStateAction<string>>;
setKeystorePassword: Dispatch<SetStateAction<string>>;
closing: MutableRefObject<((arg: () => Promise<boolean>) => void) | undefined>;
installationDetails: InstallDetails;
};
/**
* This is the network picker modal component where the user selects the desired network.
*
*
* @param props.isModalOpen the current open state of the modal
* @param props.setModalOpen a function to set the modal open state
* @param props.keyStorePath the path to the directory where the validator keys are stored
@@ -64,87 +81,133 @@ type ImportKeystoreProps = {
* @returns the import validator key modal
*/
export const ImportKeystore: FC<ImportKeystoreProps> = (props): ReactElement => {
const handleKeystorePathChange = (ev: ChangeEvent<HTMLInputElement>) => {
props.setKeystorePath(ev.target.value);
};
const handlePasswordChange = (ev: ChangeEvent<HTMLInputElement>) => {
props.setKeystorePassword(ev.target.value);
};
const handleKeystorePathChange = (ev: ChangeEvent<HTMLInputElement>) => {
props.setKeystorePath(ev.target.value)
}
const handlePasswordChange = (ev: ChangeEvent<HTMLInputElement>) => {
props.setKeystorePassword(ev.target.value)
}
const [errorUpload, setErrorUpload] = useState<boolean>(false);
function MyFormHelperText() {
// const { focused } = useFormControl() || {};
const helperText = useMemo(() => {
if (errorUpload) {
return 'provide a directory path';
} else {
return '';
}
}, [errorUpload]);
return <span>{helperText}</span>;
}
return (
<Modal
open={props.isModalOpen}
onClose={() => props.setModalOpen(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={ModalStyle}>
<Typography id="modal-modal-title" align='center' variant="h4" component="h2">
Import Validator Keys
</Typography>
<hr style={{ borderColor: 'orange' }} />
<Grid container>
<Grid xs={12} item container justifyContent={'flex-start'} direction={'column'}>
<Grid item container alignItems={'center'} p={2} spacing={2}>
<Grid item xs={6}>
<span>Keystore Directory</span>
open={props.isModalOpen}
onClose={() => props.setModalOpen(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box component="form" sx={ModalStyle}>
<Typography id="modal-modal-title" align="center" variant="h4" component="h2">
Import Validator Keys
</Typography>
<hr style={{ borderColor: 'orange' }} />
<Grid container>
<Grid xs={12} item container justifyContent={'flex-start'} direction={'column'}>
<Grid item container alignItems={'center'} p={2} spacing={2}>
<Grid item xs={6}>
<span>Keystore Directory</span>
</Grid>
<Grid item xs={6}>
<FileUploadField
helperText={<MyFormHelperText />}
placeholder="/validator_keys/"
error={errorUpload}
sx={{ my: 2, minWidth: '215', cursor: 'pointer !important' }}
variant="outlined"
onChange={handleKeystorePathChange}
value={props.keyStorePath}
required
InputProps={{
startAdornment: (
<InputAdornment
color="primary"
sx={{ paddingLeft: '14px' }}
onClick={(ev) => {
ev.preventDefault();
window.electronAPI
.invokeShowOpenDialog({
properties: ['openDirectory'],
})
.then((DialogResponse) => {
if (DialogResponse.filePaths && DialogResponse.filePaths.length) {
props.setKeystorePath(DialogResponse.filePaths[0]);
}
});
}}
position="start"
>
<FileCopy />
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Grid item xs={6}>
<FileUploadField
placeholder='/validator_keys/'
sx={{ my: 2, minWidth: '215', cursor: 'pointer !important' }}
variant="outlined"
onChange={handleKeystorePathChange}
value={props.keyStorePath}
InputProps={{
startAdornment: <InputAdornment sx={{paddingLeft: '14px'}} onClick={(ev) => {
ev.preventDefault()
window.electronAPI.invokeShowOpenDialog({
properties: ['openDirectory']
}).then(DialogResponse => {
if (DialogResponse.filePaths && DialogResponse.filePaths.length) {
props.setKeystorePath(DialogResponse.filePaths[0])
}
})
}} position="start"><FileCopy /></InputAdornment>,
}}
/>
</Grid>
</Grid>
<Grid item container alignItems={'center'} p={2} spacing={2}>
<Grid item xs={6}>
<span>Keystore Password</span>
</Grid>
<Grid item xs={6}>
<TextField
type={'password'}
sx={{ my: 2, minWidth: '215' }}
// label="Fallback URL"
variant="outlined"
value={props.keystorePassword}
InputProps={{
startAdornment: <InputAdornment position="start"><LockOpen /></InputAdornment>,
}}
onChange={handlePasswordChange}
/>
<Grid item container alignItems={'center'} p={2} spacing={2}>
<Grid item xs={6}>
<span>Keystore Password</span>
</Grid>
<Grid item xs={6}>
<TextField
type={'password'}
sx={{ my: 2, minWidth: '215' }}
// label="Fallback URL"
variant="outlined"
required
value={props.keystorePassword}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<LockOpen />
</InputAdornment>
),
}}
onChange={handlePasswordChange}
/>
</Grid>
</Grid>
</Grid>
<Grid item xs={12} textAlign="center" my={2}>
<Button
variant="contained"
color="primary"
onClick={() => {
if (!props.keyStorePath || !props.setKeystorePath.length) {
setErrorUpload(true);
return;
}
setErrorUpload(false);
props.setModalOpen(false);
if (props.closing && props.closing.current) {
props.closing.current(() =>
window.ethDocker.importKeys(
props.installationDetails.network,
props.keyStorePath,
props.keystorePassword,
),
);
}
}}
>
Import
</Button>
</Grid>
</Grid>
<Grid item xs={12} textAlign='center' my={2}>
<Button
variant="contained" color="primary" onClick={() => {
props.setModalOpen(false)
if (props.closing && props.closing.current) {
props.closing.current(() => window.ethDocker.importKeys(props.installationDetails.network, props.keyStorePath, props.keystorePassword))
}
}
}>Import</Button>
</Grid>
</Grid>
</Box>
</Modal>
)
}
</Box>
</Modal>
);
};

View File

@@ -1,18 +1,30 @@
import React, { ChangeEvent, Dispatch, FC, ReactElement, SetStateAction, useState } from 'react';
import { Grid, Typography, FormControl, Select, MenuItem, SelectChangeEvent, Modal, Box, Button, TextField, InputAdornment } from '@mui/material';
import { Folder, Link } from '@mui/icons-material'
import {
Grid,
Typography,
FormControl,
Select,
MenuItem,
SelectChangeEvent,
Modal,
Box,
Button,
TextField,
InputAdornment,
} from '@mui/material';
import { Folder, Link } from '@mui/icons-material';
import StepNavigation from '../StepNavigation';
import styled from '@emotion/styled';
import { ConsensusClients, ExecutionClients, IConsensusClient, IExecutionClient } from '../../constants'
import { ConsensusClients, ExecutionClients, IConsensusClient, IExecutionClient } from '../../constants';
import { ConsensusClient, ExecutionClient, InstallDetails } from '../../../electron/IMultiClientInstaller';
import { BackgroundLight, } from '../../colors';
import { BackgroundLight } from '../../colors';
type ConfigurationProps = {
onStepBack: () => void,
onStepForward: () => void,
installationDetails: InstallDetails,
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>
}
onStepBack: () => void;
onStepForward: () => void;
installationDetails: InstallDetails;
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>;
};
const ContentGrid = styled(Grid)`
height: 320px;
@@ -20,7 +32,6 @@ const ContentGrid = styled(Grid)`
margin-bottom: 16px;
`;
const ModalStyle = {
position: 'absolute' as 'absolute',
top: '50%',
@@ -36,45 +47,52 @@ const ModalStyle = {
/**
* This page is the second step of the install process where the user inputs their configuration.
*
*
* @param props the data and functions passed in, they are self documenting
* @returns
* @returns
*/
const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
const [consensusClient, setConsensusClient] = useState<ConsensusClient>(props.installationDetails.consensusClient);
const [executionClient, setExecutionClient] = useState<ExecutionClient>(props.installationDetails.executionClient);
const [isModalOpen, setModalOpen] = useState(false)
const [isModalOpen, setModalOpen] = useState(false);
const [checkpointSync, setCheckpointSync] = useState('');
const [executionClientFallback, setExecutionClientFallback] = useState('');
const [installationPath, setInstallationPath] = useState('');
const handleConsensusClientChange = (ev: SelectChangeEvent<string>) => {
setConsensusClient(ev.target.value as ConsensusClient)
}
setConsensusClient(ev.target.value as ConsensusClient);
};
const handleExecutionClientChange = (ev: SelectChangeEvent<string>) => {
setExecutionClient(ev.target.value as ExecutionClient)
}
setExecutionClient(ev.target.value as ExecutionClient);
};
const handleCheckpointSyncChange = (ev: ChangeEvent<HTMLInputElement>) => {
setCheckpointSync(ev.target.value)
}
setCheckpointSync(ev.target.value);
};
const handleExecutionClientFallbackChange = (ev: ChangeEvent<HTMLInputElement>) => {
setExecutionClientFallback(ev.target.value)
}
setExecutionClientFallback(ev.target.value);
};
const handleInstallationPathChange = (ev: ChangeEvent<HTMLInputElement>) => {
setInstallationPath(ev.target.value)
}
setInstallationPath(ev.target.value);
};
return (
<Grid item container direction="column" spacing={2}>
<Grid item>
<Typography variant="h1" align='center'>
<Typography variant="h1" align="center">
Configuration
</Typography>
</Grid>
<ContentGrid item container justifyContent={'center'}>
<Grid xs={11} style={{ border: '1px solid orange' }} item container justifyContent={'center'} direction={'column'}>
<Grid
xs={11}
style={{ border: '1px solid orange' }}
item
container
justifyContent={'center'}
direction={'column'}
>
<Grid item container alignItems={'center'} p={2} spacing={2}>
<Grid item xs={1}></Grid>
<Grid item xs={4}>
@@ -83,15 +101,13 @@ const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
<Grid item xs={2}></Grid>
<Grid item xs={4}>
<FormControl sx={{ my: 2, minWidth: '215' }}>
<Select
id="consensus-client"
value={consensusClient}
onChange={handleConsensusClientChange}
>
<Select id="consensus-client" value={consensusClient} onChange={handleConsensusClientChange}>
{ConsensusClients.map((c: IConsensusClient) => {
return (
<MenuItem key={c.key} value={c.key}>{c.label}</MenuItem>
)
<MenuItem key={c.key} value={c.key}>
{c.label}
</MenuItem>
);
})}
</Select>
</FormControl>
@@ -99,22 +115,20 @@ const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
<Grid item xs={1}></Grid>
</Grid>
<Grid item container alignItems={'center'} p={2} spacing={2}>
<Grid item xs={1}></Grid>
<Grid item xs={1}></Grid>
<Grid item xs={4}>
<span>Execution Client</span>
</Grid>
<Grid item xs={2}></Grid>
<Grid item xs={4}>
<FormControl sx={{ my: 2, minWidth: '215' }}>
<Select
id="execution-client"
value={executionClient}
onChange={handleExecutionClientChange}
>
<Select id="execution-client" value={executionClient} onChange={handleExecutionClientChange}>
{ExecutionClients.map((c: IExecutionClient) => {
return (
<MenuItem key={c.key} value={c.key}>{c.label}</MenuItem>
)
<MenuItem key={c.key} value={c.key}>
{c.label}
</MenuItem>
);
})}
</Select>
</FormControl>
@@ -122,7 +136,9 @@ const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
<Grid item xs={1}></Grid>
</Grid>
</Grid>
<Button disabled onClick={() => setModalOpen(true)}>Advanced Options</Button>
<Button disabled onClick={() => setModalOpen(true)}>
Advanced Options
</Button>
<Modal
open={isModalOpen}
onClose={() => setModalOpen(false)}
@@ -130,7 +146,7 @@ const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
aria-describedby="modal-modal-description"
>
<Box sx={ModalStyle}>
<Typography id="modal-modal-title" align='center' variant="h4" component="h2">
<Typography id="modal-modal-title" align="center" variant="h4" component="h2">
Advanced options
</Typography>
<hr style={{ borderColor: 'orange' }} />
@@ -148,7 +164,11 @@ const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
variant="outlined"
value={checkpointSync}
InputProps={{
startAdornment: <InputAdornment position="start"><Link /></InputAdornment>,
startAdornment: (
<InputAdornment position="start">
<Link />
</InputAdornment>
),
}}
onChange={handleCheckpointSyncChange}
/>
@@ -163,11 +183,15 @@ const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
placeholder="http://localhost:8545"
type={'url'}
sx={{ my: 2, minWidth: '215' }}
// label="Fallback URL"
// label="Fallback URL"
variant="outlined"
value={executionClientFallback}
InputProps={{
startAdornment: <InputAdornment position="start"><Link /></InputAdornment>,
startAdornment: (
<InputAdornment position="start">
<Link />
</InputAdornment>
),
}}
onChange={handleExecutionClientFallbackChange}
/>
@@ -179,14 +203,20 @@ const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
</Grid>
<Grid item xs={6}>
<TextField
placeholder='.wagyu/'
onClick={(ev) => { ev.preventDefault(); }}
placeholder=".wagyu/"
onClick={(ev) => {
ev.preventDefault();
}}
sx={{ my: 2, minWidth: '215' }}
variant="outlined"
disabled
value={installationPath}
InputProps={{
startAdornment: <InputAdornment position="start"><Folder /></InputAdornment>,
startAdornment: (
<InputAdornment position="start">
<Folder />
</InputAdornment>
),
}}
onChange={handleInstallationPathChange}
/>
@@ -201,24 +231,24 @@ const Configuration: FC<ConfigurationProps> = (props): ReactElement => {
<StepNavigation
onPrev={props.onStepBack}
onNext={() => {
let installationDetails: InstallDetails = {
debug: props.installationDetails.debug,
consensusClient,
executionClient,
network: props.installationDetails.network
}
network: props.installationDetails.network,
};
props.setInstallationDetails(installationDetails)
props.setInstallationDetails(installationDetails);
props.onStepForward()
props.onStepForward();
}}
backLabel={"Back"}
nextLabel={"Install"}
backLabel={'Back'}
nextLabel={'Install'}
disableBack={false}
disableNext={false}
/>
</Grid>
);
}
};
export default Configuration;

View File

@@ -1,19 +1,34 @@
import React, { Dispatch, FC, ReactElement, SetStateAction, useState, useRef, useEffect } from 'react';
import { Grid, Typography, Fab, CircularProgress, Box } from '@mui/material';
import StepNavigation from '../StepNavigation';
import { DoneOutline, DownloadingOutlined, ComputerOutlined, RocketLaunchOutlined, KeyOutlined, ErrorOutline } from '@mui/icons-material';
import styled from '@emotion/styled';
import React, {
Dispatch,
FC,
ReactElement,
SetStateAction,
useState,
useRef,
useEffect,
} from "react";
import { Grid, Typography, Fab, CircularProgress, Box } from "@mui/material";
import StepNavigation from "../StepNavigation";
import {
DoneOutline,
DownloadingOutlined,
ComputerOutlined,
RocketLaunchOutlined,
KeyOutlined,
ErrorOutline,
} from "@mui/icons-material";
import styled from "@emotion/styled";
import { green, red } from '@mui/material/colors';
import { InstallDetails } from '../../../electron/IMultiClientInstaller';
import { ImportKeystore } from '../ImportKeystore'
import { green, red } from "@mui/material/colors";
import { InstallDetails, OutputLogs } from "../../../electron/IMultiClientInstaller";
// import { ImportKeystore } from "../ImportKeystore";
type InstallProps = {
onStepBack: () => void,
onStepForward: () => void,
installationDetails: InstallDetails,
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>
}
onStepBack: () => void;
onStepForward: () => void;
installationDetails: InstallDetails;
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>;
};
const ContentGrid = styled(Grid)`
height: 320px;
@@ -21,230 +36,321 @@ const ContentGrid = styled(Grid)`
margin-bottom: 16px;
`;
/**
* This page is the third step of the install process where the software is being installed.
*
*
* @param props the data and functions passed in, they are self documenting
* @returns
* @returns
*/
const Install: FC<InstallProps> = (props): ReactElement => {
const [loadingPreInstall, setLoadingPreInstall] = useState(false);
const [loadingInstall, setLoadingInstall] = useState(false);
const [loadingKeyImport, setLoadingKeyImport] = useState(false);
// const [loadingKeyImport, setLoadingKeyImport] = useState(false);
const [loadingPostInstall, setLoadingPostInstall] = useState(false);
const [failedPreInstall, setFailedPreInstall] = useState(false);
const [failedInstall, setFailedInstall] = useState(false);
const [failedKeyImport, setFailedKeyImport] = useState(false);
// const [failedKeyImport, setFailedKeyImport] = useState(false);
const [failedPostInstall, setFailedPostInstall] = useState(false);
const [successPreInstall, setSuccessPreInstall] = useState(false);
const [successInstall, setSuccessInstall] = useState(false);
const [successKeyImport, setSuccessKeyImport] = useState(false);
// const [successKeyImport, setSuccessKeyImport] = useState(false);
const [successPostInstall, setSuccessPostInstall] = useState(false);
const [percentagePreInstall, setPercentagePreInstall] = useState(0);
const [percentageInstall, setPercentageInstall] = useState(0);
const [percentagePostInstall, setPercentagePostInstall] = useState(0);
// const resolveModal = useRef<(arg: () => Promise<boolean> => void)>(Promise.resolve);
const resolveModal = useRef<(arg: () => Promise<boolean>) => void>();
const [disableBack, setDisableBack] = useState<boolean>(true)
const [disableForward, setDisableForward] = useState<boolean>(true)
const [disableBack, setDisableBack] = useState<boolean>(true);
const [disableForward, setDisableForward] = useState<boolean>(true);
const buttonPreInstallSx = {
...(failedPreInstall ? {
bgcolor: '#ffc107',
'&:hover': {
bgcolor: '#ffc107',
},
} : successPreInstall && {
bgcolor: green[500],
'&:hover': {
bgcolor: green[700],
},
}),
...(failedPreInstall
? {
bgcolor: "#ffc107",
"&:hover": {
bgcolor: "#ffc107",
},
}
: successPreInstall && {
bgcolor: green[500],
"&:hover": {
bgcolor: green[700],
},
}),
};
const buttonInstallSx = {
...(failedInstall ? {
bgcolor: '#ffc107',
'&:hover': {
bgcolor: '#ffc107',
},
} :successInstall && {
bgcolor: green[500],
'&:hover': {
bgcolor: green[700],
},
}),
...(failedInstall
? {
bgcolor: "#ffc107",
"&:hover": {
bgcolor: "#ffc107",
},
}
: successInstall && {
bgcolor: green[500],
"&:hover": {
bgcolor: green[700],
},
}),
};
const buttonKeyImportSx = {
...(failedKeyImport ? {
bgcolor: '#ffc107',
'&:hover': {
bgcolor: '#ffc107',
},
} : successKeyImport && {
bgcolor: green[500],
'&:hover': {
bgcolor: green[700],
},
}),
};
// const buttonKeyImportSx = {
// ...(failedKeyImport
// ? {
// bgcolor: "#ffc107",
// "&:hover": {
// bgcolor: "#ffc107",
// },
// }
// : successKeyImport && {
// bgcolor: green[500],
// "&:hover": {
// bgcolor: green[700],
// },
// }),
// };
const buttonPostInstallSx = {
...(failedPostInstall ? {
bgcolor: '#ffc107',
'&:hover': {
bgcolor: '#ffc107',
},
} : successPostInstall && {
bgcolor: green[500],
'&:hover': {
bgcolor: green[700],
},
}),
...(failedPostInstall
? {
bgcolor: "#ffc107",
"&:hover": {
bgcolor: "#ffc107",
},
}
: successPostInstall && {
bgcolor: green[500],
"&:hover": {
bgcolor: green[700],
},
}),
};
const [isModalOpen, setModalOpen] = useState<boolean>(false);
const [keyStorePath, setKeystorePath] = useState<string>("");
const [keystorePassword, setKeystorePassword] = useState<string>("");
const [isModalOpen, setModalOpen] = useState<boolean>(false)
const [keyStorePath, setKeystorePath] = useState<string>('')
const [keystorePassword, setKeystorePassword] = useState<string>('')
const bufferLoad:() => Promise<boolean> = () => {
const bufferLoad: () => Promise<boolean> = () => {
return new Promise((resolve) => {
setTimeout(() => resolve(true), 1500)
})
}
setTimeout(() => resolve(true), 1500);
});
};
const handlePreInstall: () => Promise<boolean> = () => {
if (props.installationDetails.debug === true) {
return new Promise((resolve) => {
setSuccessPreInstall(false);
setLoadingPreInstall(true);
setTimeout(() => {
setSuccessPreInstall(true);
setLoadingPreInstall(false);
resolve(true);
}, 1500);
});
}
return new Promise((resolve) => {
if (!loadingPreInstall) {
setSuccessPreInstall(false);
setLoadingPreInstall(true);
Promise.all([window.ethDocker.preInstall(), bufferLoad()]).then((res) => {
setSuccessPreInstall(true);
setLoadingPreInstall(false);
resolve(res[0])
})
const consoleLog: OutputLogs = (message: string, progress?: number) => {
console.log(message);
if (progress) {
setPercentagePreInstall(progress);
}
};
Promise.all([window.ethDocker.preInstall(consoleLog), bufferLoad()]).then(
(res) => {
setSuccessPreInstall(true);
setLoadingPreInstall(false);
resolve(res[0]);
},
);
}
})
});
};
const handleInstall: () => Promise<boolean> = () => {
if (props.installationDetails.debug === true) {
return new Promise((resolve) => {
setSuccessInstall(false);
setLoadingInstall(true);
setTimeout(() => {
setSuccessInstall(true);
setLoadingInstall(false);
resolve(true);
}, 1500);
});
}
return new Promise((resolve) => {
if (!loadingInstall) {
setSuccessInstall(false);
setLoadingInstall(true);
Promise.all([window.ethDocker.install(props.installationDetails), bufferLoad()]).then((res) => {
const installConsole: OutputLogs = (message: string, progress?: number) => {
console.log("UPDATING LOG:", message, progress);
console.log(message);
if (progress) {
setPercentageInstall(progress);
}
};
console.log("STARTING INSTALL");
Promise.all([
window.ethDocker.install(props.installationDetails, installConsole),
bufferLoad(),
]).then((res) => {
setSuccessInstall(true);
setLoadingInstall(false);
resolve(res[0])
})
resolve(res[0]);
});
}
})
});
};
const handleKeyImportModal: () => Promise<boolean> = () => {
return new Promise((resolve: (arg: () => Promise<boolean>) => void) => {
setSuccessKeyImport(false)
setLoadingKeyImport(true);
setModalOpen(true)
resolveModal.current = resolve
}).then((keyImp: () => Promise<boolean>) => {
return new Promise((resolve: (arg: boolean) => void ) => {
Promise.all([keyImp(), bufferLoad()]).then(res => {
setSuccessKeyImport(true)
setLoadingKeyImport(false);
resolve(res[0])
}).catch(err => {
console.error('error importing key: ',err)
setLoadingKeyImport(false);
resolve(false)
})
})
}).catch(err => {
console.error('error filling out key import modal: ',err)
setLoadingKeyImport(false);
return false
})
}
// const handleKeyImportModal: () => Promise<boolean> = () => {
// if (props.installationDetails.debug === true) {
// return new Promise((resolve) => {
// setSuccessKeyImport(false);
// setLoadingKeyImport(true);
// setTimeout(() => {
// setSuccessKeyImport(true);
// setLoadingKeyImport(false);
// resolve(true);
// }, 1500);
// });
// }
// return new Promise((resolve: (arg: () => Promise<boolean>) => void) => {
// setSuccessKeyImport(false);
// setLoadingKeyImport(true);
// setModalOpen(true);
// resolveModal.current = resolve;
// })
// .then((keyImp: () => Promise<boolean>) => {
// return new Promise((resolve: (arg: boolean) => void) => {
// Promise.all([keyImp(), bufferLoad()])
// .then((res) => {
// setSuccessKeyImport(true);
// setLoadingKeyImport(false);
// resolve(res[0]);
// })
// .catch((err) => {
// console.error("error importing key: ", err);
// setLoadingKeyImport(false);
// resolve(false);
// });
// });
// })
// .catch((err) => {
// console.error("error filling out key import modal: ", err);
// setLoadingKeyImport(false);
// return false;
// });
// };
const handlePostInstall: () => Promise<boolean> = () => {
if (props.installationDetails.debug === true) {
return new Promise((resolve) => {
setSuccessPostInstall(false);
setLoadingPostInstall(true);
setTimeout(() => {
setSuccessPostInstall(true);
setLoadingPostInstall(false);
resolve(true);
}, 1500);
});
}
return new Promise((resolve) => {
if (!loadingPostInstall) {
setSuccessPostInstall(false);
setLoadingPostInstall(true);
Promise.all([ window.ethDocker.postInstall(props.installationDetails.network), bufferLoad()]).then((res) => {
setSuccessPostInstall(true);
setLoadingPostInstall(false);
resolve(res[0])
})
const postInstallConsole: OutputLogs = (message: string, progress?: number) => {
console.log(message);
if (progress) {
setPercentagePostInstall(progress);
}
};
Promise.all([
window.ethDocker.postInstall(
props.installationDetails.network,
postInstallConsole,
),
bufferLoad(),
]).then((res) => {
setSuccessPostInstall(true);
setLoadingPostInstall(false);
resolve(res[0]);
});
}
})
});
};
const install = async (step: number) => {
if (!step) {
step = 0
step = 0;
}
switch (step) {
case 0:
setFailedPreInstall(false)
let preInstallResult = await handlePreInstall()
setFailedPreInstall(false);
let preInstallResult = await handlePreInstall();
if (!preInstallResult) {
setFailedPreInstall(true)
setDisableBack(false)
return
setFailedPreInstall(true);
setDisableBack(false);
return;
}
step = 1
step = 1;
case 1:
setFailedInstall(false)
let installResult = await handleInstall()
setFailedInstall(false);
let installResult = await handleInstall();
if (!installResult) {
setFailedInstall(true)
setDisableBack(false)
return
setFailedInstall(true);
setDisableBack(false);
return;
}
step += 1
case 2:
setFailedKeyImport(false)
let keyImportResult = await handleKeyImportModal()
if (!keyImportResult) {
setFailedKeyImport(true)
setDisableBack(false)
return
}
step += 1
step += 2;
// case 2:
// setFailedKeyImport(false);
// let keyImportResult = await handleKeyImportModal();
// if (!keyImportResult) {
// setFailedKeyImport(true);
// setDisableBack(false);
// return;
// }
// step += 1;
case 3:
setFailedPostInstall(false)
let postInstallResult = await handlePostInstall()
setFailedPostInstall(false);
let postInstallResult = await handlePostInstall();
if (!postInstallResult) {
setFailedPostInstall(true)
setDisableBack(false)
return
setFailedPostInstall(true);
setDisableBack(false);
return;
}
}
setDisableForward(false)
setDisableBack(true)
}
setDisableForward(false);
setDisableBack(true);
};
useEffect(() => {
install(0)
}, [])
install(0);
}, []);
return (
<Grid item container direction="column" spacing={2}>
<Grid item>
<Typography variant="h1" align='center'>
<Typography variant="h1" align="center">
Installing
</Typography>
</Grid>
@@ -252,149 +358,174 @@ const [keystorePassword, setKeystorePassword] = useState<string>('')
<Grid item container>
<Grid item xs={3}></Grid>
<Grid item container justifyContent="center" alignItems="center" xs={2}>
<Box sx={{ m: 1, position: 'relative' }}>
<Fab
<Box sx={{ m: 1, position: "relative" }}>
<Fab
aria-label="save"
color="primary"
sx={buttonPreInstallSx}
disabled={!failedPreInstall}
onClick={failedPreInstall ? () => install(0) : () => {}}
>
{
!failedPreInstall ? successPreInstall ? <DoneOutline sx={{
color: green[500],
}}/> : <DownloadingOutlined /> : <ErrorOutline sx={{ color: red[500] }} />
}
{!failedPreInstall ? (
successPreInstall ? (
<DoneOutline
sx={{
color: green[500],
}}
/>
) : (
<DownloadingOutlined />
)
) : (
<ErrorOutline sx={{ color: red[500] }} />
)}
</Fab>
{loadingPreInstall && (
<CircularProgress
size={68}
sx={{
color: green[500],
position: 'absolute',
position: "absolute",
top: -6,
left: -6,
zIndex: 1,
}}
/>
)}
</Box>
</Box>
</Grid>
<Grid item xs={1}></Grid>
<Grid item container justifyContent="flex-start" alignItems="center" xs={3}>
<span>Downloading dependencies</span>
<span>Downloading dependencies ({percentagePreInstall}%)</span>
</Grid>
<Grid item xs={3}></Grid>
</Grid>
<Grid item container>
<Grid item xs={3}></Grid>
<Grid item container justifyContent="center" alignItems="center" xs={2}>
<Box sx={{ m: 1, position: 'relative' }}>
<Fab
<Box sx={{ m: 1, position: "relative" }}>
<Fab
aria-label="save"
color="primary"
sx={buttonInstallSx}
disabled={!failedInstall}
onClick={failedInstall ? () => install(1) : () => {}}
>
{
!failedInstall ? successInstall ? <DoneOutline sx={{
color: green[500],
}} /> : <ComputerOutlined /> : <ErrorOutline sx={{ color: red[500] }} />
}
{!failedInstall ? (
successInstall ? (
<DoneOutline
sx={{
color: green[500],
}}
/>
) : (
<ComputerOutlined />
)
) : (
<ErrorOutline sx={{ color: red[500] }} />
)}
</Fab>
{loadingInstall && (
<CircularProgress
size={68}
sx={{
color: green[500],
position: 'absolute',
position: "absolute",
top: -6,
left: -6,
zIndex: 1,
}}
/>
)}
</Box>
</Box>
</Grid>
<Grid item xs={1}></Grid>
<Grid item container justifyContent="flex-start" alignItems="center" xs={4}>
<span>Installing services</span>
<span>Installing services ({percentageInstall}%)</span>
</Grid>
<Grid item xs={3}></Grid>
</Grid>
{/* <Grid item container>
<Grid item xs={3}></Grid>
<Grid item container justifyContent="center" alignItems="center" xs={2}>
<Box sx={{ m: 1, position: "relative" }}>
<Fab aria-label="save" color="primary" sx={buttonKeyImportSx} disabled={!failedKeyImport} onClick={failedKeyImport ? () => install(2) : () => {}}>
{!failedKeyImport ? (
successKeyImport ? (
<DoneOutline
sx={{
color: green[500],
}}
/>
) : (
<KeyOutlined />
)
) : (
<ErrorOutline sx={{ color: red[500] }} />
)}
</Fab>
{loadingKeyImport && (
<CircularProgress
size={68}
sx={{
color: green[500],
position: "absolute",
top: -6,
left: -6,
zIndex: 1,
}}
/>
)}
</Box>
</Grid>
<Grid item xs={1}></Grid>
<Grid item container justifyContent="flex-start" alignItems="center" xs={4}>
<span>Key Import</span>
</Grid>
<Grid item xs={3}></Grid>
</Grid> */}
<Grid item container>
<Grid item xs={3}></Grid>
<Grid item container justifyContent="center" alignItems="center" xs={2}>
<Box sx={{ m: 1, position: 'relative' }}>
<Fab
aria-label="save"
color="primary"
sx={buttonKeyImportSx}
disabled={!failedKeyImport}
onClick={failedKeyImport ? () => install(2) : () => {}}
>
{
!failedKeyImport ? successKeyImport ? <DoneOutline sx={{
color: green[500],
}} /> : <KeyOutlined />: <ErrorOutline sx={{ color: red[500] }} />
}
</Fab>
{loadingKeyImport && (
<CircularProgress
size={68}
sx={{
color: green[500],
position: 'absolute',
top: -6,
left: -6,
zIndex: 1,
}}
/>
)}
</Box>
</Grid>
<Grid item xs={1}></Grid>
<Grid item container justifyContent="flex-start" alignItems="center" xs={4}>
<span>Key Import</span>
</Grid>
<Grid item xs={3}></Grid>
</Grid>
<Grid item container>
<Grid item xs={3}></Grid>
<Grid item container justifyContent="center" alignItems="center" xs={2}>
<Box sx={{ m: 1, position: 'relative' }}>
<Fab
<Box sx={{ m: 1, position: "relative" }}>
<Fab
aria-label="save"
color="primary"
sx={buttonPostInstallSx}
disabled={!failedPostInstall}
onClick={failedPostInstall ? () => install(3) : () => {}}
>
{
!failedPostInstall ? successPostInstall ?
<DoneOutline sx={{
color: green[500],
}} /> : <RocketLaunchOutlined /> : <ErrorOutline sx={{ color: red[500] }} />
}
{!failedPostInstall ? (
successPostInstall ? (
<DoneOutline
sx={{
color: green[500],
}}
/>
) : (
<RocketLaunchOutlined />
)
) : (
<ErrorOutline sx={{ color: red[500] }} />
)}
</Fab>
{loadingPostInstall && (
<CircularProgress
size={68}
sx={{
color: green[500],
position: 'absolute',
position: "absolute",
top: -6,
left: -6,
zIndex: 1,
}}
/>
)}
</Box>
</Box>
</Grid>
<Grid item xs={1}></Grid>
<Grid item container justifyContent="flex-start" alignItems="center" xs={4}>
<span>Configuring and launching</span>
<span>Configuring and launching ({percentagePostInstall}%)</span>
</Grid>
<Grid item xs={3}></Grid>
</Grid>
@@ -409,18 +540,18 @@ const [keystorePassword, setKeystorePassword] = useState<string>('')
disableBack={disableBack}
disableNext={disableForward}
/>
<ImportKeystore
setModalOpen={setModalOpen}
isModalOpen={isModalOpen}
setKeystorePassword={setKeystorePassword}
setKeystorePath={setKeystorePath}
keyStorePath={keyStorePath}
keystorePassword={keystorePassword}
closing={resolveModal}
installationDetails={props.installationDetails}
/>
{/* <ImportKeystore
setModalOpen={setModalOpen}
isModalOpen={isModalOpen}
setKeystorePassword={setKeystorePassword}
setKeystorePath={setKeystorePath}
keyStorePath={keyStorePath}
keystorePassword={keystorePassword}
closing={resolveModal}
installationDetails={props.installationDetails}
/> */}
</Grid>
);
}
};
export default Install;

View File

@@ -13,7 +13,7 @@ const SoftText = styled(Typography)`
const Container = styled.div`
position: fixed;
bottom: 35;
bottom: 15;
width: 100%;
`;

View File

@@ -0,0 +1,98 @@
import { BackgroundLight } from "../../colors";
import { Typography, Box, Grid, Modal, IconButton, Tooltip } from "@mui/material";
import React, { FC, Dispatch, ReactElement, SetStateAction } from "react";
import { FormatPubkey } from "../../../utility";
import { HighlightOff } from "@mui/icons-material";
const ModalStyle = {
position: "absolute" as "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 600,
padding: "20px",
borderRadius: "20px",
background: BackgroundLight,
boxShadow: 24,
p: 4,
};
type ManageValidatorsProps = {
setModalOpen: Dispatch<SetStateAction<boolean>>;
isModalOpen: boolean;
validators: Array<string>;
};
const handleDeleteValidator = () => {};
const renderValidators: (v: Array<string>) => JSX.Element[] = (
validators: Array<string>,
): JSX.Element[] => {
let rows = [];
for (let i = 0; i < validators.length; i++) {
rows.push(
<Grid key={validators[i]} container paddingX={1} paddingY={2} alignItems="center">
<Grid xs={4} item>
{FormatPubkey(validators[i])}
</Grid>
<Grid xs={4} item>
Running
</Grid>
<Grid xs={4} item container justifyContent="flex-end">
<Grid item>
<Tooltip title="Delete Validator">
<IconButton color="error" onClick={handleDeleteValidator} tabIndex={1}>
<HighlightOff />
</IconButton>
</Tooltip>
</Grid>
</Grid>
</Grid>,
);
}
return rows;
};
/**
* This is the network picker modal component where the user selects the desired network.
*
* @param props.isModalOpen the current open state of the modal
* @param props.setModalOpen a function to set the modal open state
* @param props.validators is an array of validator public keys that are imported
* @returns the import validator key modal
*/
export const ManageValidators: FC<ManageValidatorsProps> = (props): ReactElement => {
return (
<Modal
open={props.isModalOpen}
onClose={() => props.setModalOpen(false)}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box component="form" sx={ModalStyle}>
<Typography id="modal-modal-title" align="center" variant="h4" component="h2">
Manage Validators
</Typography>
<hr style={{ borderColor: "orange" }} />
<Grid container paddingX={1} alignItems="center">
<Grid xs={4} item>
<Typography marginY={1} align="left" variant="h3" component="h2">
Pubkey
</Typography>
</Grid>
<Grid xs={4} item>
<Typography marginY={1} align="left" variant="h3" component="h2">
Status
</Typography>
</Grid>
<Grid xs={4} item>
<Typography marginY={1} align="right" variant="h3" component="h2">
{/* Action */}
</Typography>
</Grid>
</Grid>
{renderValidators(props.validators)}
</Box>
</Modal>
);
};

View File

@@ -1,63 +1,68 @@
import { ConsensusClient, ExecutionClient } from '../electron/IMultiClientInstaller';
import { StepKey } from './types';
import { ConsensusClient, ExecutionClient } from "../electron/IMultiClientInstaller";
import { StepKey } from "./types";
export const errors = {
FOLDER: "Please select a folder.",
FOLDER_DOES_NOT_EXISTS: "Folder does not exist. Select an existing folder.",
FOLDER_IS_NOT_WRITABLE: "Cannot write in this folder. Select a folder in which you have write permission.",
FOLDER: "Please select a folder.",
FOLDER_DOES_NOT_EXISTS: "Folder does not exist. Select an existing folder.",
FOLDER_IS_NOT_WRITABLE:
"Cannot write in this folder. Select a folder in which you have write permission.",
};
export const stepLabels = {
[StepKey.SystemCheck]: 'System Check',
[StepKey.Configuration]: 'Configuration',
[StepKey.Installing]: 'Install',
[StepKey.SystemCheck]: "System Check",
[StepKey.Configuration]: "Configuration",
[StepKey.Installing]: "Install",
};
export const nullAddress = "0x0000000000000000000000000000000000000000";
export interface IExecutionClient {
key: ExecutionClient;
label: string;
key: ExecutionClient;
label: string;
}
export interface IConsensusClient {
key: ConsensusClient;
label: string;
key: ConsensusClient;
label: string;
}
export const ConsensusClients: IConsensusClient[] = [{
key: ConsensusClient.PRYSM,
label: 'Prysm',
},
{
key: ConsensusClient.LIGHTHOUSE,
label: 'Lighthouse',
}, {
key: ConsensusClient.NIMBUS,
label: 'Nimbus',
}, {
key: ConsensusClient.TEKU,
label: 'Teku'
},
{
key: ConsensusClient.LODESTAR,
label: 'Lodestar'
},
]
export const ExecutionClients: IExecutionClient[] = [{
key: ExecutionClient.GETH,
label: 'Geth',
},
{
key: ExecutionClient.ERIGON,
label: 'Erigon'
},
{
key: ExecutionClient.BESU,
label: 'Besu'
},
{
key: ExecutionClient.NETHERMIND,
label: 'Nethermind'
},
]
export const ConsensusClients: IConsensusClient[] = [
{
key: ConsensusClient.PRYSM,
label: "Prysm",
},
{
key: ConsensusClient.LIGHTHOUSE,
label: "Lighthouse",
},
{
key: ConsensusClient.NIMBUS,
label: "Nimbus",
},
{
key: ConsensusClient.TEKU,
label: "Teku",
},
{
key: ConsensusClient.LODESTAR,
label: "Lodestar",
},
];
export const ExecutionClients: IExecutionClient[] = [
{
key: ExecutionClient.GETH,
label: "Geth",
},
{
key: ExecutionClient.ERIGON,
label: "Erigon",
},
{
key: ExecutionClient.BESU,
label: "Besu",
},
{
key: ExecutionClient.NETHERMIND,
label: "Nethermind",
},
];

View File

@@ -1,6 +1,8 @@
<html>
<body style="width:100%;height:100%;margin:0">
<div id="app" style="width:100%;height:100%">
</div>
<body style="width: 100%; height: 100%; margin: 0">
<div id="app" style="width: 100%; height: 100%"></div>
<script>
window.global = {};
</script>
</body>
</html>
</html>

View File

@@ -5,6 +5,5 @@ import App from "./App";
// We find our app DOM element as before
const app = document.getElementById("app");
// Finally, we render our top-level component to the actual DOM.
ReactDOM.render(<App />, app);

View File

@@ -1,45 +1,45 @@
import React, { FC, ReactElement, SetStateAction, useState, Dispatch } from 'react';
import React, { FC, ReactElement, SetStateAction, useState, Dispatch } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { Stepper, Step, StepLabel, Grid, Typography } from '@mui/material';
import styled from '@emotion/styled';
import { StepKey } from '../types';
import { stepLabels } from '../constants';
import { StepSequenceKey } from '../types';
import VersionFooter from '../components/VersionFooter';
import Install from '../components/InstallFlow/2-Install';
import Configuration from '../components/InstallFlow/1-Configuration';
import SystemCheck from '../components/InstallFlow/0-SystemCheck';
import { InstallDetails } from '../../electron/IMultiClientInstaller';
import { Stepper, Step, StepLabel, Grid, Typography } from "@mui/material";
import styled from "@emotion/styled";
import { StepKey } from "../types";
import { stepLabels } from "../constants";
import { StepSequenceKey } from "../types";
import VersionFooter from "../components/VersionFooter";
import Install from "../components/InstallFlow/2-Install";
import Configuration from "../components/InstallFlow/1-Configuration";
import SystemCheck from "../components/InstallFlow/0-SystemCheck";
import { InstallDetails } from "../../electron/IMultiClientInstaller";
const stepSequenceMap: Record<string, StepKey[]> = {
install: [
// StepKey.SystemCheck,
StepKey.Configuration,
StepKey.Installing,
]
}
],
};
const MainGrid = styled(Grid)`
`;
const StyledStepper = styled(Stepper)`
background-color: transparent;
`
`;
type RouteParams = {
stepSequenceKey: StepSequenceKey;
};
type WizardProps = {
installationDetails: InstallDetails,
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>
}
installationDetails: InstallDetails;
setInstallationDetails: Dispatch<SetStateAction<InstallDetails>>;
};
/**
* This is the main wizard through which each piece of functionality for the app runs.
*
*
* This wizard manages the global stepper showing the user where they are in the process.
*
*
* @param props passed in data for the component to use
* @returns the react element to render
*/
@@ -52,17 +52,16 @@ const Wizard: FC<WizardProps> = (props): ReactElement => {
const stepSequence = stepSequenceMap[stepSequenceKey as string];
const activeStepKey = stepSequence[activeStepIndex];
const onStepForward = () => {
if (activeStepIndex === stepSequence.length - 1) {
const location = {
pathname: `/systemOverview`
}
pathname: `/dashboard`,
};
navigate(location);
}
setActiveStepIndex(activeStepIndex + 1);
}
};
const onStepBack = () => {
if (activeStepIndex === 0) {
@@ -70,7 +69,7 @@ const Wizard: FC<WizardProps> = (props): ReactElement => {
} else {
setActiveStepIndex(activeStepIndex - 1);
}
}
};
/**
* This is the UI stepper component rendering where the user is in the process
@@ -87,7 +86,6 @@ const Wizard: FC<WizardProps> = (props): ReactElement => {
</Grid>
);
const commonProps = {
onStepForward,
onStepBack,
@@ -103,21 +101,15 @@ const Wizard: FC<WizardProps> = (props): ReactElement => {
const stepComponentSwitch = (): ReactElement => {
switch (activeStepKey) {
case StepKey.SystemCheck:
return (
<SystemCheck {...{ ...commonProps }} />
);
return <SystemCheck {...{ ...commonProps }} />;
case StepKey.Configuration:
return (
<Configuration {...{ ...commonProps }} />
);
return <Configuration {...{ ...commonProps }} />;
case StepKey.Installing:
return (
<Install {...{ ...commonProps }} />
);
return <Install {...{ ...commonProps }} />;
default:
return <div>No component for this step</div>
return <div>No component for this step</div>;
}
}
};
return (
<MainGrid container direction="column">
@@ -135,6 +127,6 @@ const Wizard: FC<WizardProps> = (props): ReactElement => {
<VersionFooter />
</MainGrid>
);
}
};
export default Wizard;

View File

@@ -1,44 +1,911 @@
import React, { FC, ReactElement } from "react";
import { Grid, Typography } from '@mui/material';
import styled from '@emotion/styled';
import React, { FC, ReactElement, useState, useRef, useMemo } from "react";
import {
Grid,
Typography,
Button,
IconButton,
Tooltip,
Alert,
Snackbar,
AlertColor,
} from "@mui/material";
import styled from "@emotion/styled";
import VersionFooter from "../components/VersionFooter";
import { InstallDetails } from "../../electron/IMultiClientInstaller";
import { Capitalize } from "../../utility";
import {
Settings,
Add,
Stop,
PlayArrow,
Notifications,
Upgrade,
} from "@mui/icons-material";
import { ImportKeystore } from "../components/ImportKeystore";
import { ManageValidators } from "../components/modals/ManageValidators";
const MainGrid = styled(Grid)`
width: 100%;
width: inherit;
height: inherit;
margin: 0px;
text-align: center;
`;
const Card = styled(Grid)`
background: rgba(0, 94, 86, 0.2);
border: 1px solid rgba(255, 166, 0, 0.3);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
`;
const HorizontalLine = styled("div")`
position: absolute;
top: 25%;
bottom: 25%;
border-left: 1px solid rgba(255, 166, 0, 0.3);
left: calc(50%);
`;
type SystemOverviewProps = {
installationDetails: InstallDetails
}
installationDetails: InstallDetails;
};
export type NodeState =
| "syncing"
| "installing"
| "synced"
| "waiting"
| "stopped"
| "stopping"
| "starting";
const SystemOverview: FC<SystemOverviewProps> = (props): ReactElement => {
const [statusConsensusNode, setStatusConsensusNode] = useState<NodeState>("syncing");
const [consensusUpdateAvailable, setConsensusUpdateAvailable] = useState<boolean>(true);
const [peersConsensusNode, setPeersConsensusNode] = useState(0);
const [currentEpoch, setCurrentEpoch] = useState(0);
const [syncTimeLeft, setSyncTimeLeft] = useState("1 hour");
const [consensusVersion, setConsensusVersion] = useState("2.1");
const UpdateConsensusNodeIcon = () =>
useMemo(() => {
if (consensusUpdateAvailable) {
return (
<Tooltip title="Upgrade Consensus Node">
<IconButton color="primary" onClick={handleConsensusNodeUpdate} tabIndex={1}>
<Upgrade />
</IconButton>
</Tooltip>
);
} else {
return (
<IconButton disabled color="primary" tabIndex={1}>
<Upgrade />
</IconButton>
);
}
}, [consensusUpdateAvailable]);
const handleConsensusNodeUpdate = () => {
setStatusConsensusNode("installing");
setConsensusUpdateAvailable(false);
setTimeout(() => {
setStatusConsensusNode("starting");
setTimeout(() => {
setStatusConsensusNode("synced");
}, 3000);
}, 3000);
};
const StartStopConsensusNodeIcon = () =>
useMemo(() => {
switch (statusConsensusNode) {
case "syncing":
return (
<Tooltip title="Stop Consensus Node">
<IconButton
color="primary"
onClick={toggleRunningStateConsensus}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
case "installing":
return (
<IconButton disabled color="primary" tabIndex={1}>
<Stop />
</IconButton>
);
case "stopping":
return (
<IconButton disabled color="primary" tabIndex={1}>
<PlayArrow />
</IconButton>
);
case "stopped":
return (
<Tooltip title="Start Consensus Node">
<IconButton
color="primary"
onClick={toggleRunningStateConsensus}
tabIndex={1}
>
<PlayArrow />
</IconButton>
</Tooltip>
);
case "starting":
return (
<IconButton disabled color="primary" tabIndex={1}>
<Stop />
</IconButton>
);
case "waiting":
return (
<Tooltip title="Stop Consensus Node">
<IconButton
color="primary"
onClick={toggleRunningStateConsensus}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
case "synced":
return (
<Tooltip title="Stop Consensus Node">
<IconButton
color="primary"
onClick={toggleRunningStateConsensus}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
}
}, [statusConsensusNode]);
const toggleRunningStateConsensus = () => {
if (statusConsensusNode === "stopped") {
setStatusConsensusNode("starting");
setTimeout(() => {
setStatusConsensusNode("synced");
}, 3000);
} else {
setStatusConsensusNode("stopping");
setTimeout(() => {
setStatusConsensusNode("stopped");
}, 3000);
}
};
const [statusValidator, setStatusValidator] = useState<NodeState>("waiting");
const [validatorVersion, setValidatorVersion] = useState("2.1");
const StartStopValidatorClientIcon = () =>
useMemo(() => {
switch (statusValidator) {
case "syncing":
return (
<Tooltip title="Stop Validator Client">
<IconButton
color="primary"
onClick={toggleRunningStateValidator}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
case "installing":
return (
<IconButton
disabled
color="primary"
onClick={toggleRunningStateValidator}
tabIndex={1}
>
<Stop />
</IconButton>
);
case "stopping":
return (
<IconButton
disabled
color="primary"
onClick={toggleRunningStateValidator}
tabIndex={1}
>
<PlayArrow />
</IconButton>
);
case "stopped":
return (
<Tooltip title="Start Validator Client">
<IconButton
color="primary"
onClick={toggleRunningStateValidator}
tabIndex={1}
>
<PlayArrow />
</IconButton>
</Tooltip>
);
case "starting":
return (
<IconButton
disabled
color="primary"
onClick={toggleRunningStateValidator}
tabIndex={1}
>
<Stop />
</IconButton>
);
case "waiting":
return (
<Tooltip title="Stop Validator Client">
<IconButton
color="primary"
onClick={toggleRunningStateValidator}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
case "synced":
return (
<Tooltip title="Stop Validator client">
<IconButton
color="primary"
onClick={toggleRunningStateValidator}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
}
}, [statusValidator]);
const toggleRunningStateValidator = () => {
if (statusValidator === "stopped") {
setStatusValidator("starting");
setTimeout(() => {
setStatusValidator("synced");
}, 3000);
} else {
setStatusValidator("stopping");
setTimeout(() => {
setStatusValidator("stopped");
}, 3000);
}
};
const [statusExecutionNode, setStatusExecutionNode] = useState<NodeState>("syncing");
const [peersExecutionNode, setPeersExecutionNode] = useState(0);
const [executionVersion, setExecutionVersion] = useState("2.1");
const [executionUpdateAvailable, setExecutionUpdateAvailable] = useState<boolean>(true);
const UpdateExecutionNodeIcon = () =>
useMemo(() => {
if (executionUpdateAvailable) {
return (
<Tooltip title="Upgrade Consensus Node">
<IconButton color="primary" onClick={handleExecutionNodeUpdate} tabIndex={1}>
<Upgrade />
</IconButton>
</Tooltip>
);
} else {
return (
<IconButton disabled color="primary" tabIndex={1}>
<Upgrade />
</IconButton>
);
}
}, [executionUpdateAvailable]);
const handleExecutionNodeUpdate = () => {
setStatusExecutionNode("installing");
setExecutionUpdateAvailable(false);
setTimeout(() => {
setStatusExecutionNode("starting");
setTimeout(() => {
setStatusExecutionNode("synced");
}, 3000);
}, 3000);
};
const StartStopExecutionClientIcon = () =>
useMemo(() => {
switch (statusExecutionNode) {
case "syncing":
return (
<Tooltip title="Stop Execution Node">
<IconButton
color="primary"
onClick={toggleRunningStateExecutionNode}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
case "installing":
return (
<IconButton
disabled
color="primary"
onClick={toggleRunningStateExecutionNode}
tabIndex={1}
>
<Stop />
</IconButton>
);
case "stopping":
return (
<IconButton
disabled
color="primary"
onClick={toggleRunningStateExecutionNode}
tabIndex={1}
>
<PlayArrow />
</IconButton>
);
case "stopped":
return (
<Tooltip title="Start Execution Node">
<IconButton
color="primary"
onClick={toggleRunningStateExecutionNode}
tabIndex={1}
>
<PlayArrow />
</IconButton>
</Tooltip>
);
case "starting":
return (
<IconButton
disabled
color="primary"
onClick={toggleRunningStateExecutionNode}
tabIndex={1}
>
<Stop />
</IconButton>
);
case "waiting":
return (
<Tooltip title="Stop Execution Node">
<IconButton
color="primary"
onClick={toggleRunningStateExecutionNode}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
case "synced":
return (
<Tooltip title="Stop Execution Node">
<IconButton
color="primary"
onClick={toggleRunningStateExecutionNode}
tabIndex={1}
>
<Stop />
</IconButton>
</Tooltip>
);
}
}, [statusExecutionNode]);
const toggleRunningStateExecutionNode = () => {
if (statusValidator === "stopped") {
setStatusExecutionNode("starting");
setTimeout(() => {
setStatusExecutionNode("synced");
}, 3000);
} else {
setStatusExecutionNode("stopping");
setTimeout(() => {
setStatusExecutionNode("stopped");
}, 3000);
}
};
const [isManageValidatorModalOpen, setManageValidatorModalOpen] =
useState<boolean>(false);
const [isImportKeyModalOpen, setImportKeyModalOpen] = useState<boolean>(false);
const [keyStorePath, setKeystorePath] = useState<string>("");
const [keystorePassword, setKeystorePassword] = useState<string>("");
const resolveModal = useRef<(arg: () => Promise<boolean>) => void>();
new Promise((resolve: (arg: () => Promise<boolean>) => void) => {
resolveModal.current = resolve;
})
.then((keyImp: () => Promise<boolean>) => {
keyImp()
.then((key) => {
console.log(key);
if (key) {
toggleAlert("Keystore import successful", "success");
} else {
toggleAlert("Keystore import failed", "error");
}
})
.catch((err) => {
toggleAlert("Keystore import failed", "error");
console.error("error key import modal rejected with", err);
});
})
.catch((err) => {
toggleAlert("Keystore import failed", "error");
console.error("error key import modal rejected with", err);
});
const [alertOpen, setAlertOpen] = useState(false);
const [alertMessage, setAlertMessage] = useState("This is an error");
const [alertSeverity, setAlertSeverity] = useState("success" as AlertColor);
const toggleAlert = (message: string, severity: AlertColor) => {
setAlertMessage(message);
setAlertSeverity(severity);
setAlertOpen(true);
};
const handleAlertClose = (event?: React.SyntheticEvent | Event, reason?: string) => {
if (reason === "clickaway") {
return;
}
setAlertOpen(false);
};
return (
<MainGrid container spacing={5} direction="column">
<Grid item container>
<Grid item xs={10} />
<Grid item xs={2}>
<Typography variant="caption" style={{ color: "gray" }}>
Network: {props.installationDetails.network}
</Typography>
<MainGrid container direction={"column"}>
<Grid xs={1} item>
<Typography variant="h1" align="center">
Overview
</Typography>
</Grid>
<Grid xs={11} container item>
<Grid
xs={6}
paddingY={2}
paddingX={1}
container
item
direction={"column"}
justifyContent="space-around"
>
<Grid xs={2} item container>
<Grid container item justifyContent="space-between" alignItems="center">
<Grid item xs={6}>
<Typography variant="h3" align="left">
{Capitalize(props.installationDetails.consensusClient)} Node
<span style={{ fontSize: "1rem", opacity: "0.8" }}>
{" "}
v{consensusVersion}
</span>
</Typography>
</Grid>
<Grid container item xs={6} justifyContent="flex-end">
<Grid item>
<UpdateConsensusNodeIcon />
</Grid>
<Grid item>
<StartStopConsensusNodeIcon />
</Grid>
</Grid>
</Grid>
</Grid>
<Card xs={9} position="relative" item container justifyContent="stretch">
<HorizontalLine />
<Grid xs={12} padding={2} container item alignItems="center">
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Status</Grid>
<Grid item>
<span>{statusConsensusNode}</span>
</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Epoch</Grid>
<Grid item>{currentEpoch}</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Peers</Grid>
<Grid item>{peersConsensusNode}</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Remaining</Grid>
<Grid item>{syncTimeLeft}</Grid>
</Grid>
{/* <Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>DB Path</Grid>
<Grid item>/home/</Grid>
</Grid> */}
{/* <Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Node</Grid>
<Grid item>Local</Grid>
</Grid> */}
</Grid>
</Card>
</Grid>
<Grid
xs={6}
paddingY={2}
paddingX={1}
container
item
direction={"column"}
justifyContent="space-around"
>
<Grid xs={2} item container alignItems="center">
<Grid container item justifyContent="space-between" alignItems="center">
<Grid item xs={6}>
<Typography variant="h3" align="left">
Validator Client
<span style={{ fontSize: "1rem", opacity: "0.8" }}>
{" "}
v{validatorVersion}
</span>
</Typography>
</Grid>
<Grid xs={6} container item justifyContent="flex-end">
<Grid item>
<Tooltip title="Add Validator">
<IconButton
color="primary"
onClick={() => {
setImportKeyModalOpen(true);
}}
tabIndex={1}
>
<Add />
</IconButton>
</Tooltip>
</Grid>
<Grid item>
<Tooltip title="Manage Validators">
<IconButton
color="primary"
onClick={() => {
setManageValidatorModalOpen(true);
}}
tabIndex={1}
>
<Settings />
</IconButton>
</Tooltip>
</Grid>
<Grid item>
<StartStopValidatorClientIcon />
</Grid>
</Grid>
</Grid>
</Grid>
<Card xs={9} position="relative" item container justifyContent="stretch">
<HorizontalLine />
<Grid xs={12} padding={2} container item alignItems="center">
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Status</Grid>
<Grid item>{statusValidator}</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>
<span>Validators</span>
</Grid>
<Grid item>5</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>DB Path</Grid>
<Grid item>/home/</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Node</Grid>
<Grid item>Local</Grid>
</Grid>
</Grid>
</Card>
</Grid>
<Grid
xs={6}
paddingY={2}
paddingX={1}
container
item
direction={"column"}
justifyContent="space-around"
>
<Grid xs={2} item alignItems="center">
<Grid container item justifyContent="space-between" alignItems="center">
<Grid xs={6} item>
<Typography variant="h3" align="left">
{Capitalize(props.installationDetails.executionClient)} Node
<span style={{ fontSize: "1rem", opacity: "0.8" }}>
{" "}
v{executionVersion}
</span>
</Typography>
</Grid>
<Grid container item xs={6} justifyContent="flex-end">
<Grid item>
<UpdateExecutionNodeIcon />
</Grid>
<Grid item>
<StartStopExecutionClientIcon />
</Grid>
</Grid>
</Grid>
</Grid>
<Card xs={9} position="relative" item container justifyContent="stretch">
<HorizontalLine />
<Grid xs={12} padding={2} container item alignItems="center">
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Status</Grid>
<Grid item>{statusExecutionNode}</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Remaining</Grid>
<Grid item>1 hour</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>DB Path</Grid>
<Grid item>/home/</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Peers</Grid>
<Grid item>{peersExecutionNode}</Grid>
</Grid>
</Grid>
</Card>
</Grid>
<Grid
xs={6}
paddingY={2}
paddingX={1}
container
item
direction={"column"}
justifyContent="space-around"
>
<Grid xs={2} item alignItems="center">
<Grid container item justifyContent="space-between" alignItems="center">
<Grid xs={6} item>
<Typography variant="h3" align="left">
System
</Typography>
</Grid>
<Grid container item xs={6} justifyContent="flex-end">
<Grid item>
<Tooltip title="Configure alerts">
<IconButton
color="primary"
onClick={() => {
window.open("https://beaconcha.in/user/notifications");
}}
tabIndex={1}
>
<Notifications />
</IconButton>
</Tooltip>
</Grid>
</Grid>
</Grid>
</Grid>
<Card xs={9} position="relative" item container justifyContent="stretch">
<HorizontalLine />
<Grid xs={12} padding={2} container item alignItems="center">
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Storage</Grid>
<Grid item>600 GB</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>RAM</Grid>
<Grid item>3 GB</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>Growth</Grid>
<Grid item>10 GB/mo</Grid>
</Grid>
<Grid
justifyContent="space-between"
container
xs={6}
paddingY={1}
paddingX={2}
item
>
<Grid item>CPU</Grid>
<Grid item>30%</Grid>
</Grid>
</Grid>
</Card>
</Grid>
</Grid>
<Grid item>
<Typography variant="h1">
{/* System Overview */}
Installation Complete
</Typography>
<ul style={{ margin: '0 auto', width: '350px', marginTop: '3rem', textAlign: 'left'}}>
<li>Network: {props.installationDetails.network}</li>
<li>Consensus Client: {props.installationDetails.consensusClient}</li>
<li>Execution Client: {props.installationDetails.executionClient}</li>
</ul>
</Grid>
<VersionFooter />
<ImportKeystore
setModalOpen={setImportKeyModalOpen}
isModalOpen={isImportKeyModalOpen}
setKeystorePassword={setKeystorePassword}
setKeystorePath={setKeystorePath}
keyStorePath={keyStorePath}
keystorePassword={keystorePassword}
closing={resolveModal}
installationDetails={props.installationDetails}
/>
<ManageValidators
setModalOpen={setManageValidatorModalOpen}
isModalOpen={isManageValidatorModalOpen}
validators={[
"97754fa1198496c4918f30ad7dc634cc5dceea36bf7d27c2e2c6c798b88484e458811e2d8ef833b8d750b83ae63dc33a",
"94bb7f930d0d142d97a99cc9d6b98c04f247eda6d463b699bcc0460be371b9b7e22922bb10239f11b1fc44937cd16b8e",
]}
/>
<Snackbar
open={alertOpen}
autoHideDuration={6000}
onClose={handleAlertClose}
anchorOrigin={{ vertical: "top", horizontal: "left" }}
>
<Alert onClose={handleAlertClose} severity={alertSeverity} sx={{ width: "100%" }}>
{alertMessage}
</Alert>
</Snackbar>
</MainGrid>
);
};
export default SystemOverview;
{
/* <Grid item container>
<Grid item xs={10} />
<Grid item xs={2}>
<Typography variant="caption" style={{ color: "gray" }}>
Network: {props.installationDetails.network}
</Typography>
</Grid>
</Grid>
<Grid item>
<Typography variant="h1">
Installation Complete
</Typography>
<ul style={{ margin: '0 auto', width: '350px', marginTop: '3rem', textAlign: 'left'}}>
<li>Network: {props.installationDetails.network}</li>
<li>Consensus Client: {props.installationDetails.consensusClient}</li>
<li>Execution Client: {props.installationDetails.executionClient}</li>
</ul>
</Grid> */
}

View File

@@ -19,6 +19,12 @@ const theme = createTheme({
].join(','),
h1: {
fontSize: "36px"
},
h2: {
fontSize: "26px",
},
h3: {
fontSize: "1.4rem",
}
}
});

View File

@@ -1,24 +1,50 @@
export enum StepKey {
SystemCheck,
Configuration,
Installing
Installing,
}
export enum StepSequenceKey {
Install = "install"
Install = "install",
}
export enum Network {
PRATER = "Prater",
MAINNET = "Mainnet"
MAINNET = "Mainnet",
SEPOLIA = "Sepolia",
}
export enum ExecutionNetwork {
GOERLI = "goerli",
MAINNET = "mainnet"
MAINNET = "mainnet",
SEPOLIA = "Sepolia",
}
export const networkToExecution: Map<Network, ExecutionNetwork> = new Map([
[Network.PRATER, ExecutionNetwork.GOERLI],
[Network.MAINNET, ExecutionNetwork.MAINNET]
]);
[Network.MAINNET, ExecutionNetwork.MAINNET],
[Network.SEPOLIA, ExecutionNetwork.SEPOLIA],
]);
export interface BeaconMeta {
count: number;
}
export interface BeaconResponse<T> {
data: T;
meta: BeaconMeta;
}
export interface BeaconGetSyncingStatus {
head_slot: string;
sync_distance: string;
is_syncing: boolean;
is_optimistic: boolean;
}
export interface BeaconGetPeers {
peer_id: string;
enr: string;
last_seen_p2p_address: string;
state: string;
direction: string;
}

14
src/utility.ts Normal file
View File

@@ -0,0 +1,14 @@
export function Capitalize(text: string) {
if (text && text.length) {
return text[0].toUpperCase() + text.substring(1)
}
return text
}
export function FormatPubkey(pubkey: string): string {
if(pubkey && pubkey.length > 6) {
// return pubkey.substring(0, 3) + "…" + pubkey.substring(-3, 3)
return pubkey.substring(0, 6) + "..." + pubkey.substring(pubkey.length -6)
}
return pubkey
}

View File

@@ -1,21 +1,20 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./build", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"outDir": "./build", /* Redirect output structure to the directory. */
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
@@ -23,9 +22,8 @@
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
@@ -33,7 +31,6 @@
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
@@ -41,7 +38,6 @@
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
@@ -50,22 +46,19 @@
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
}

13
webpack.devserver.js Normal file
View File

@@ -0,0 +1,13 @@
const path = require('path');
module.exports = {
//...
devServer: {
static: {
directory: path.resolve(__dirname, 'static'),
},
compress: true,
port: 9000,
mode: 'development',
},
};

View File

@@ -1,4 +1,3 @@
'use strict';
// pull in the 'path' module from node
@@ -32,13 +31,16 @@ module.exports = {
{
test: /\.tsx?$/,
loader: 'ts-loader',
}, {
},
{
test: /node_modules\/JSONStream\/index\.js$/,
loader: 'shebang-loader'
}, {
loader: 'shebang-loader',
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}, {
},
{
test: /\.(woff|woff2|eot|ttf|svg)$/,
type: 'asset/resource',
},
@@ -59,7 +61,8 @@ module.exports = {
COMMITHASH: JSON.stringify(gitRevisionPlugin.commithash()),
BRANCH: JSON.stringify(gitRevisionPlugin.branch()),
LASTCOMMITDATETIME: JSON.stringify(gitRevisionPlugin.lastcommitdatetime()),
})
}),
],
target: 'electron-renderer'
// target: 'electron-renderer'
target: 'web',
};

6935
yarn.lock

File diff suppressed because it is too large Load Diff