diff --git a/.storybook/main.js b/.storybook/main.js index b76c73eb5..6df644877 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -22,27 +22,27 @@ module.exports = { use: ['style-loader', 'css-loader', 'sass-loader'], include: path.resolve(__dirname, '../'), }, - // SVG + // SVG + { + test: /\.svg$/, + issuer: /\.[jt]sx?$/, + use: [ { - test: /\.svg$/, - issuer: /\.[jt]sx?$/, - use: [ - { - loader: '@svgr/webpack', - options: { - prettier: false, - svgo: false, - svgoConfig: { - plugins: [{ removeViewBox: false }], - }, - titleProp: true, - ref: true, - }, + loader: '@svgr/webpack', + options: { + prettier: false, + svgo: false, + svgoConfig: { + plugins: [{ removeViewBox: false }], }, - 'file-loader', - ], - // include: path.resolve(__dirname, '../') + titleProp: true, + ref: true, + }, }, + 'file-loader', + ], + // include: path.resolve(__dirname, '../') + }, { test: /\.(png|woff|woff2|eot|ttf|svg)$/, include: path.resolve(__dirname, '../') diff --git a/assets/locales/en/systemRequirements.json b/assets/locales/en/systemRequirements.json index 00f395b39..1f8ca1830 100644 --- a/assets/locales/en/systemRequirements.json +++ b/assets/locales/en/systemRequirements.json @@ -12,5 +12,7 @@ "dockerInstalledTitle" : "Docker is installed", "dockerVersionInstalledTitle" : "Docker version {{minVersion}} or later installed", "dockerVersionInstalledDescription" : "Docker version: {{version}}", - "dockerVersionInstalledNeedsUpdateCaption": "Please update Docker from the Docker app" + "dockerVersionInstalledNeedsUpdateCaption": "Please update Docker from the Docker app", + "dockerNotInstalledDescription": "Docker can be installed in the next step.", + "dockerCaption": "Docker helps software work on a wide range of computers." } diff --git a/assets/locales/en/translation.json b/assets/locales/en/translation.json index c84862279..11a8c0585 100644 --- a/assets/locales/en/translation.json +++ b/assets/locales/en/translation.json @@ -17,6 +17,7 @@ "created":"created", "initializing":"initializing", "checkingForUpdates":"checking for updates", + "download":"download", "downloading":"downloading", "downloaded":"downloaded", "errorDownloading":"error downloading", @@ -36,6 +37,10 @@ "dockerPurpose": "Docker helps NiceNode provide more node and client options for users. Docker is free for users to install.", "restartDockerOnInstall": "Restart NiceNode when Docker is installed and running.", "ensureDockerIsRunning": "If you have Docker Desktop installed, ensure it is running.", + "DownloadingDocker": "Downloading Docker...", + "InstallingDocker": "Installing Docker...", + "DockerInstallComplete": "Installation complete!", + "DownloadedSomeMegaBytesOfTotal": "{{downloadedBytes}}MB of {{totalBytes}}MB downloaded", "installDockerOnYourOwn": "Install Docker on your own.", "dockerInstallGuide": "Docker Desktop install guide", "dockerInstallStatus": "Docker install status:", diff --git a/package-lock.json b/package-lock.json index f7601ebbf..1808f4f4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "react-tooltip": "^4.2.21", "styled-components": "^5.3.5", "systeminformation": "^5.12.6", + "throttle-debounce": "^5.0.0", "uuid": "^8.3.2", "winston": "^3.7.2", "winston-daily-rotate-file": "^4.6.1" @@ -32038,6 +32039,14 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "node_modules/throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "engines": { + "node": ">=12.22" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -59354,6 +59363,11 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index fb40d7b0e..79918b96a 100644 --- a/package.json +++ b/package.json @@ -307,6 +307,7 @@ "react-tooltip": "^4.2.21", "styled-components": "^5.3.5", "systeminformation": "^5.12.6", + "throttle-debounce": "^5.0.0", "uuid": "^8.3.2", "winston": "^3.7.2", "winston-daily-rotate-file": "^4.6.1" diff --git a/src/common/NodeSpecs/besu/besu-v1.0.0.json b/src/common/NodeSpecs/besu/besu-v1.0.0.json index 0c6985f4e..d6ed30660 100644 --- a/src/common/NodeSpecs/besu/besu-v1.0.0.json +++ b/src/common/NodeSpecs/besu/besu-v1.0.0.json @@ -22,6 +22,26 @@ }, "category": "L1/ExecutionClient", "rpcTranslation": "eth-l1", + "systemRequirements": { + "documentationUrl": "https://besu.hyperledger.org/en/stable/public-networks/get-started/system-requirements/", + "cpu" : { + "cores": 4 + }, + "memory": { + "minSizeGBs": 8 + }, + "storage" : { + "minSizeGBs" : 750, + "ssdRequired": true + }, + "internet" : { + "minDownloadSpeedMbps": 10, + "minUploadSpeedMbps": 10 + }, + "docker" : { + "required": true + } + }, "configTranslation": { "dataDir": { "displayName": "Node data is stored in this folder", diff --git a/src/common/NodeSpecs/geth/geth-v1.0.0.json b/src/common/NodeSpecs/geth/geth-v1.0.0.json index 94bbd5d49..f544b62b9 100644 --- a/src/common/NodeSpecs/geth/geth-v1.0.0.json +++ b/src/common/NodeSpecs/geth/geth-v1.0.0.json @@ -33,6 +33,26 @@ }, "category": "L1/ExecutionClient", "rpcTranslation": "eth-l1", + "systemRequirements": { + "documentationUrl": "https://geth.ethereum.org/docs/interface/hardware", + "cpu" : { + "cores": 4 + }, + "memory": { + "minSizeGBs": 16 + }, + "storage" : { + "minSizeGBs" : 2000, + "ssdRequired": true + }, + "internet" : { + "minDownloadSpeedMbps": 25, + "minUploadSpeedMbps": 10 + }, + "docker" : { + "required": true + } + }, "configTranslation": { "dataDir": { "displayName": "Node data is stored in this folder", diff --git a/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json b/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json index 7d34f1202..42ba9074e 100644 --- a/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json +++ b/src/common/NodeSpecs/lighthouse/lighthouse-v1.0.0.json @@ -30,6 +30,28 @@ "responseFormat": "githubReleases" } }, + "category": "L1/ConsensusClient/BeaconNode", + "rpcTranslation": "eth-l1-beacon", + "systemRequirements": { + "documentationUrl": "https://lighthouse-book.sigmaprime.io/system-requirements.html", + "cpu" : { + "cores": 2 + }, + "memory": { + "minSizeGBs": 8 + }, + "storage" : { + "minSizeGBs" : 128, + "ssdRequired": true + }, + "internet" : { + "minDownloadSpeedMbps": 10, + "minUploadSpeedMbps": 5 + }, + "docker" : { + "required": true + } + }, "configTranslation": { "dataDir": { "displayName": "Node data is stored in this folder", @@ -113,8 +135,6 @@ "documentation": "https://lighthouse-book.sigmaprime.io/advanced_networking.html#target-peers" } }, - "category": "L1/ConsensusClient/BeaconNode", - "rpcTranslation": "eth-l1-beacon", "documentation": { "default": "https://lighthouse-book.sigmaprime.io/intro.html", "docker": "https://lighthouse-book.sigmaprime.io/docker.html" diff --git a/src/common/NodeSpecs/lodestar/lodestar-v1.0.0.json b/src/common/NodeSpecs/lodestar/lodestar-v1.0.0.json index 0535caa31..6ed93154a 100644 --- a/src/common/NodeSpecs/lodestar/lodestar-v1.0.0.json +++ b/src/common/NodeSpecs/lodestar/lodestar-v1.0.0.json @@ -21,7 +21,26 @@ }, "category": "L1/ConsensusClient/BeaconNode", "rpcTranslation": "eth-l1-beacon", - "nodeReleasePhase": "beta", + "systemRequirements": { + "documentationUrl": "https://chainsafe.github.io/lodestar/#specifications", + "cpu" : { + "cores": 4 + }, + "memory": { + "minSizeGBs": 4 + }, + "storage" : { + "minSizeGBs" : 20, + "ssdRequired": true + }, + "internet" : { + "minDownloadSpeedMbps": 10, + "minUploadSpeedMbps": 10 + }, + "docker" : { + "required": true + } + }, "configTranslation": { "dataDir": { "displayName": "Node data is stored in this folder", diff --git a/src/common/NodeSpecs/nethermind/nethermind-v1.0.0.json b/src/common/NodeSpecs/nethermind/nethermind-v1.0.0.json index 99aa74287..c60b4e616 100644 --- a/src/common/NodeSpecs/nethermind/nethermind-v1.0.0.json +++ b/src/common/NodeSpecs/nethermind/nethermind-v1.0.0.json @@ -31,6 +31,27 @@ }, "category": "L1/ExecutionClient", "rpcTranslation": "eth-l1", + "systemRequirements": { + "documentationUrl": "https://docs.nethermind.io/nethermind/guides-and-helpers/validator-setup/eth2-validator#hardware-and-network-requirements", + "cpu" : { + "cores": 4, + "minSpeedGHz": 2.4 + }, + "memory": { + "minSizeGBs": 16 + }, + "storage" : { + "minSizeGBs" : 900, + "ssdRequired": true + }, + "internet" : { + "minDownloadSpeedMbps": 10, + "minUploadSpeedMbps": 10 + }, + "docker" : { + "required": true + } + }, "configTranslation": { "dataDir": { "displayName": "Node data is stored in this folder", diff --git a/src/common/NodeSpecs/nimbus/nimbus-v1.0.0.json b/src/common/NodeSpecs/nimbus/nimbus-v1.0.0.json index 304c68715..e88a78856 100644 --- a/src/common/NodeSpecs/nimbus/nimbus-v1.0.0.json +++ b/src/common/NodeSpecs/nimbus/nimbus-v1.0.0.json @@ -28,6 +28,22 @@ }, "category": "L1/ConsensusClient/BeaconNode", "rpcTranslation": "eth-l1-beacon", + "systemRequirements": { + "memory": { + "minSizeGBs": 4 + }, + "storage" : { + "minSizeGBs" : 200, + "ssdRequired": true + }, + "internet" : { + "minDownloadSpeedMbps": 8, + "minUploadSpeedMbps": 8 + }, + "docker" : { + "required": true + } + }, "configTranslation": { "dataDir": { "displayName": "The directory where nimbus will store all blockchain data.", diff --git a/src/common/NodeSpecs/prysm/prysm-v1.0.0.json b/src/common/NodeSpecs/prysm/prysm-v1.0.0.json index 87fd0931b..f2b3b9dba 100644 --- a/src/common/NodeSpecs/prysm/prysm-v1.0.0.json +++ b/src/common/NodeSpecs/prysm/prysm-v1.0.0.json @@ -16,6 +16,26 @@ }, "category": "L1/ConsensusClient/BeaconNode", "rpcTranslation": "eth-l1-beacon", + "systemRequirements": { + "documentationUrl": "https://docs.prylabs.network/docs/install/install-with-script#step-1-review-prerequisites-and-best-practices", + "cpu" : { + "cores": 4 + }, + "memory": { + "minSizeGBs": 8 + }, + "storage" : { + "minSizeGBs" : 200, + "ssdRequired": true + }, + "internet" : { + "minDownloadSpeedMbps": 8, + "minUploadSpeedMbps": 8 + }, + "docker" : { + "required": true + } + }, "configTranslation": { "dataDir": { "displayName": "The directory where nimbus will store all blockchain data.", diff --git a/src/common/NodeSpecs/teku/teku-v1.0.0.json b/src/common/NodeSpecs/teku/teku-v1.0.0.json index 92c4ee50b..4a780724f 100644 --- a/src/common/NodeSpecs/teku/teku-v1.0.0.json +++ b/src/common/NodeSpecs/teku/teku-v1.0.0.json @@ -23,6 +23,26 @@ }, "category": "L1/ConsensusClient/BeaconNode", "rpcTranslation": "eth-l1-beacon", + "systemRequirements": { + "documentationUrl": "https://docs.teku.consensys.net/en/stable/HowTo/Get-Started/Connect/Connect-To-Mainnet/", + "cpu" : { + "cores": 4 + }, + "memory": { + "minSizeGBs": 8 + }, + "storage" : { + "minSizeGBs" : 200, + "ssdRequired": true + }, + "internet" : { + "minDownloadSpeedMbps": 8, + "minUploadSpeedMbps": 8 + }, + "docker" : { + "required": true + } + }, "configTranslation": { "dataDir": { "displayName": "Node data is stored in this folder", diff --git a/src/common/nodeSpec.ts b/src/common/nodeSpec.ts index 196e0a3b9..d356a2fc7 100644 --- a/src/common/nodeSpec.ts +++ b/src/common/nodeSpec.ts @@ -1,3 +1,4 @@ +import { SystemRequirements } from './systemRequirements'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { ConfigValuesMap, ConfigTranslationMap } from './nodeConfig'; import { NiceNodeRpcTranslation } from './rpcTranslation'; @@ -88,6 +89,7 @@ export type NodeSpecification = { version: string; displayName: string; execution: NodeExecution; + systemRequirements?: SystemRequirements; rpcTranslation?: NiceNodeRpcTranslation; configTranslation?: ConfigTranslationMap; nodeReleasePhase?: 'alpha' | 'beta'; diff --git a/src/common/systemRequirements.ts b/src/common/systemRequirements.ts index 74d12aa02..6aaa8489a 100644 --- a/src/common/systemRequirements.ts +++ b/src/common/systemRequirements.ts @@ -1,26 +1,27 @@ export type CpuRequirements = { cores?: number; - minSpeed?: number; + minSpeedGHz?: number; }; export type MemoryRequirements = { - minSize?: number; - minSpeed?: number; + minSizeGBs?: number; + minSpeedGHz?: number; }; export type StorageRequirements = { - minSize?: number; - minWriteSpeed?: number; + minSizeGBs?: number; + minWriteSpeedMBps?: number; ssdRequired?: boolean; // NVMe? }; export type InternetRequirements = { - minDownloadSpeed: number; - minUploadSpeed: number; + minDownloadSpeedMbps?: number; + minUploadSpeedMbps?: number; + noDataCapRecommended?: boolean; // and {{minUploadSpeed}} up // etherenetRequired?: boolean; // latency? }; export type DockerRequirements = { - required: boolean; + required?: boolean; minVersion?: string; }; diff --git a/src/main/docker/installOnMac.ts b/src/main/docker/installOnMac.ts index aea711dc2..ac01ebc30 100644 --- a/src/main/docker/installOnMac.ts +++ b/src/main/docker/installOnMac.ts @@ -3,6 +3,7 @@ import * as arch from '../arch'; import logger from '../logger'; import { execAwait } from '../execHelper'; import { downloadFile } from '../downloadFile'; +import { sendMessageOnDownloadProgress } from './messageFrontEnd'; /** * Download docker.dmg, install docker, start docker @@ -22,7 +23,11 @@ const installOnMac = async (): Promise => { }; } logger.info(`Downloading Docker from url ${downloadUrl}`); - const dockerDmgFilePath = await downloadFile(downloadUrl, getNNDirPath()); + const dockerDmgFilePath = await downloadFile( + downloadUrl, + getNNDirPath(), + sendMessageOnDownloadProgress + ); let stdout; let stderr; ({ stdout, stderr } = await execAwait( diff --git a/src/main/docker/installOnWindows.ts b/src/main/docker/installOnWindows.ts index dd819ebaa..c8f7dd133 100644 --- a/src/main/docker/installOnWindows.ts +++ b/src/main/docker/installOnWindows.ts @@ -4,6 +4,7 @@ import { execAwait } from '../execHelper'; import * as arch from '../arch'; import { downloadFile } from '../downloadFile'; import { getNNDirPath } from '../files'; +import { sendMessageOnDownloadProgress } from './messageFrontEnd'; const iconv = require('iconv-lite'); @@ -86,7 +87,11 @@ const installOnWindows = async (): Promise => { }; } logger.info(`Downloading Docker from url ${downloadUrl}`); - const dockerExeFilePath = await downloadFile(downloadUrl, getNNDirPath()); + const dockerExeFilePath = await downloadFile( + downloadUrl, + getNNDirPath(), + sendMessageOnDownloadProgress + ); ({ stdout, stderr } = await execAwait( `start /w "" "${dockerExeFilePath}" install --quiet --accept-license --backend=wsl-2`, { log: true } diff --git a/src/main/docker/messageFrontEnd.ts b/src/main/docker/messageFrontEnd.ts new file mode 100644 index 000000000..ed78935ce --- /dev/null +++ b/src/main/docker/messageFrontEnd.ts @@ -0,0 +1,8 @@ +import { FileDownloadProgress } from '../downloadFile'; +import { CHANNELS, send } from '../messenger'; + +export const sendMessageOnDownloadProgress = ( + downloadProgress: FileDownloadProgress +) => { + send(CHANNELS.docker, downloadProgress); +}; diff --git a/src/main/downloadFile.ts b/src/main/downloadFile.ts index b74567af3..ccf170593 100644 --- a/src/main/downloadFile.ts +++ b/src/main/downloadFile.ts @@ -1,8 +1,9 @@ import path from 'node:path'; -import { pipeline } from 'node:stream'; +import { pipeline, Transform } from 'node:stream'; import { promisify } from 'node:util'; import { createWriteStream } from 'fs'; import { chmod } from 'fs/promises'; +import { throttle } from 'throttle-debounce'; import { doesFileOrDirExist } from './files'; import logger from './logger'; @@ -11,6 +12,10 @@ import { httpGet } from './httpReq'; const streamPipeline = promisify(pipeline); +export type FileDownloadProgress = { + totalBytes: number; + downloadedBytes: number; +}; /** * Downloads the file to directory. * @param downloadUrl @@ -19,7 +24,8 @@ const streamPipeline = promisify(pipeline); */ export const downloadFile = async ( downloadUrl: string, - directory: string + directory: string, + progressListener?: (progress: FileDownloadProgress) => void ): Promise => { logger.info(`downloading file ${downloadUrl}`); // todo: return error if no file in url @@ -35,10 +41,46 @@ export const downloadFile = async ( }); // if (!res.ok) throw new Error(`unexpected response ${res.statusText}`); logger.info('http response received'); + logger.info('http response', response); + console.log( + 'http response content-length', + response.headers['content-length'] + ); + console.log( + 'http response Content-Length', + response.headers['Content-Length'] + ); + let totalBytes = 0; + if (response.headers['content-length']) { + totalBytes = parseInt(response.headers['content-length'], 10); + } const fileWriteStream = createWriteStream(fileOutPath); logger.info('piping response to fileWriteStream'); - await streamPipeline(response, fileWriteStream); + + let downloadedBytes = 0; + const throttleProgressListener = throttle(1000, (currBytes: number) => { + const update: FileDownloadProgress = { + totalBytes, + downloadedBytes: currBytes, + }; + console.log('update: ', update); + if (progressListener) { + progressListener(update); + } + }); + const streamProgress = new Transform({ + transform(chunk, encoding, callback) { + downloadedBytes += chunk.length; + throttleProgressListener(downloadedBytes); + this.push(chunk); + callback(); + }, + }); + await streamPipeline(response, streamProgress, fileWriteStream); + // } else { + // await streamPipeline(response, fileWriteStream); + // } logger.info( 'done piping response to fileWriteStream. closing fileWriteStream.' ); diff --git a/src/main/messenger.ts b/src/main/messenger.ts index 3c682440c..c2efb4ab7 100644 --- a/src/main/messenger.ts +++ b/src/main/messenger.ts @@ -20,5 +20,6 @@ export const send = (channel: string, ...args: any[]): void => { export const CHANNELS = { userNodes: 'userNodes', nodeLogs: 'nodeLogs', + docker: 'docker', }; -export const CHANNELS_ARRAY = ['userNodes', 'nodeLogs']; +export const CHANNELS_ARRAY = ['userNodes', 'nodeLogs', 'docker']; diff --git a/src/renderer/AddNode/AddNode.tsx b/src/renderer/AddNode/AddNode.tsx index 080a0e212..e1a4b8d52 100644 --- a/src/renderer/AddNode/AddNode.tsx +++ b/src/renderer/AddNode/AddNode.tsx @@ -10,40 +10,7 @@ import { NodeSpecification } from '../../common/nodeSpec'; // import { DopeButton } from '../DivButton'; import { NodeLibrary } from '../../main/state/nodeLibrary'; import electron from '../electronGlobal'; - -const categorizeNodeLibrary = ( - nodeLibrary: NodeLibrary -): { - ExecutionClient: NodeSpecification[]; - BeaconNode: NodeSpecification[]; - L2: NodeSpecification[]; - Other: NodeSpecification[]; -} => { - const ec: NodeSpecification[] = []; - const bn: NodeSpecification[] = []; - const l2: NodeSpecification[] = []; - const other: NodeSpecification[] = []; - - const catgorized = { - ExecutionClient: ec, - BeaconNode: bn, - L2: l2, - Other: other, - }; - Object.keys(nodeLibrary).forEach((specId) => { - const nodeSpec = nodeLibrary[specId]; - if (nodeSpec.category === 'L1/ExecutionClient') { - catgorized.ExecutionClient.push(nodeSpec); - } else if (nodeSpec.category === 'L1/ConsensusClient/BeaconNode') { - catgorized.BeaconNode.push(nodeSpec); - } else if (nodeSpec.category?.includes('L2')) { - catgorized.L2.push(nodeSpec); - } else { - catgorized.Other.push(nodeSpec); - } - }); - return catgorized; -}; +import { categorizeNodeLibrary } from '../utils'; const AddNode = () => { const { t } = useTranslation(); diff --git a/src/renderer/AddNode/AddNodeRedesign.tsx b/src/renderer/AddNode/AddNodeRedesign.tsx new file mode 100644 index 000000000..1bbfe9c97 --- /dev/null +++ b/src/renderer/AddNode/AddNodeRedesign.tsx @@ -0,0 +1,70 @@ +import { BsPlusSquareDotted } from 'react-icons/bs'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import IconButton from '../IconButton'; +import { Modal } from '../Generics/redesign/Modal/Modal'; +import ConfirmAddNode from './ConfirmAddNode'; +import { NodeSpecification } from '../../common/nodeSpec'; +// import { DopeButton } from '../DivButton'; +import electron from '../electronGlobal'; +import AddNodeStepper from '../Presentational/AddNodeStepper/AddNodeStepper'; +// todo: remove when new ui/ux redesign is further along +import { darkTheme, lightTheme } from '../Generics/redesign/theme.css'; + +const AddNode = () => { + const { t } = useTranslation(); + const [sIsModalOpenAddNode, setIsModalOpenAddNode] = useState(); + const [sIsModalOpenConfirmAddNode, setIsModalOpenConfirmAddNode] = + useState(false); + const [sSelectedNodeSpecification, setSelectedNodeSpecification] = + useState(); + + const onNodeSelected = (nodeSpec: NodeSpecification) => { + // set selected node + setSelectedNodeSpecification(nodeSpec); + // open confirm add modal + setIsModalOpenConfirmAddNode(true); + }; + + const onConfirmAddNode = () => { + // close both modals + setIsModalOpenConfirmAddNode(false); + setIsModalOpenAddNode(false); + }; + + const onClickAddNodeButton = async () => { + setIsModalOpenAddNode(true); + }; + + return ( +
+ {t('Add Node')} + + + +
+ setIsModalOpenAddNode(false)} + isFullScreen + > + { + console.log(newValue); + }} + /> + +
+ + setIsModalOpenConfirmAddNode(false)} + nodeSpec={sSelectedNodeSpecification} + /> +
+ ); +}; +export default AddNode; diff --git a/src/renderer/Generics/redesign/Button/button.css.ts b/src/renderer/Generics/redesign/Button/button.css.ts index e69629e8c..7421a25f7 100644 --- a/src/renderer/Generics/redesign/Button/button.css.ts +++ b/src/renderer/Generics/redesign/Button/button.css.ts @@ -17,6 +17,10 @@ export const baseButton = style({ color: vars.color.fontDisabled, background: vars.color.backgroundDisabled, }, + // offsets legacy value in app.css + ':hover': { + transform: 'none', + }, fontFamily: 'Inter', fontWeight: 500, fontSize: 13, diff --git a/src/renderer/Generics/redesign/Checklist/Checklist.tsx b/src/renderer/Generics/redesign/Checklist/Checklist.tsx index 145e881ae..6f05c5ffd 100644 --- a/src/renderer/Generics/redesign/Checklist/Checklist.tsx +++ b/src/renderer/Generics/redesign/Checklist/Checklist.tsx @@ -20,12 +20,12 @@ export interface ChecklistProps { export const Checklist = ({ title, items }: ChecklistProps) => { return (
-

{title}

+ {title &&

{title}

} {items && - items.map((item) => ( + items.map((item, index) => ( - + {index !== items.length - 1 && } ))}
diff --git a/src/renderer/Generics/redesign/LabelValues/LabelValuesSection.tsx b/src/renderer/Generics/redesign/LabelValues/LabelValuesSection.tsx index 775b83ff2..1e05d15e4 100644 --- a/src/renderer/Generics/redesign/LabelValues/LabelValuesSection.tsx +++ b/src/renderer/Generics/redesign/LabelValues/LabelValuesSection.tsx @@ -30,7 +30,7 @@ const LabelValuesSection = ({ {items && items.map((item) => ( - +
{item.label}
{item.value}
diff --git a/src/renderer/Generics/redesign/Link/externalLink.css.ts b/src/renderer/Generics/redesign/Link/externalLink.css.ts index 043a9f2ee..285648905 100644 --- a/src/renderer/Generics/redesign/Link/externalLink.css.ts +++ b/src/renderer/Generics/redesign/Link/externalLink.css.ts @@ -20,4 +20,6 @@ export const linkText = style({ ':hover': { color: vars.color.primaryHover, }, + // todo: remove when app.css is removed. Legacy fix + margin: 0, }); diff --git a/src/renderer/Generics/redesign/Modal/Modal.tsx b/src/renderer/Generics/redesign/Modal/Modal.tsx new file mode 100644 index 000000000..c11df9fa8 --- /dev/null +++ b/src/renderer/Generics/redesign/Modal/Modal.tsx @@ -0,0 +1,51 @@ +import { CgCloseO } from 'react-icons/cg'; + +import IconButton from '../../../IconButton'; +import { modalBackdropStyle, modalContentStyle } from './modal.css'; + +type Props = { + children: React.ReactNode; + isOpen: boolean | undefined; + onClickCloseButton: () => void; + title: string; + isFullScreen?: boolean; +}; + +export const Modal = ({ + children, + isOpen, + onClickCloseButton, + title, + isFullScreen, +}: Props) => { + return ( +
+
+
+

{title}

+ + + +
+
{children}
+
+
+ ); +}; diff --git a/src/renderer/Generics/redesign/Modal/modal.css.ts b/src/renderer/Generics/redesign/Modal/modal.css.ts new file mode 100644 index 000000000..331edfe9a --- /dev/null +++ b/src/renderer/Generics/redesign/Modal/modal.css.ts @@ -0,0 +1,31 @@ +import { style } from '@vanilla-extract/css'; +import { common, vars } from '../theme.css'; + +export const modalBackdropStyle = style({ + display: 'none', + position: 'fixed', + zIndex: 1, + left: '0', + top: '0', + width: '100%', + height: '100%', + overflow: 'auto', + backgroundColor: 'rgba(0, 0, 2, 0.25)', +}); + +export const modalContentStyle = style({ + filter: + 'drop-shadow(0px 32px 64px rgba(0, 0, 0, 0.1876)) drop-shadow(0px 2px 21px rgba(0, 0, 0, 0.1474))', + maxHeight: '95vh', + backgroundColor: vars.color.background, + padding: '20px', + paddingTop: '0px', + borderRadius: 14, + width: '80%', + top: '50%', + left: '50%', + position: 'fixed', + transform: 'translate(-50%, -50%)', + color: 'inherit', + // a: { color: 'inherit' }, +}); diff --git a/src/renderer/Generics/redesign/NodeIcon/NodeIcon.tsx b/src/renderer/Generics/redesign/NodeIcon/NodeIcon.tsx new file mode 100644 index 000000000..631c367d1 --- /dev/null +++ b/src/renderer/Generics/redesign/NodeIcon/NodeIcon.tsx @@ -0,0 +1,75 @@ +import { + NODE_ICONS, + NodeIconId, + NODE_COLORS, +} from '../../../assets/images/nodeIcons'; +import { + imageStyle, + iconBackground, + largeStyle, + mediumStyle, + hasStatusStyle, + smallStyle, + statusStyle, + containerStyle, + sync, + error, + healthy, + warning, +} from './nodeIcon.css'; + +export interface NodeIconProps { + /** + * Which icon? // TODO: Change this to drop down eventually + */ + iconId: NodeIconId; + /** + * What's the status? + */ + status?: 'healthy' | 'warning' | 'error' | 'sync'; + /** + * What size should the icon be? + */ + size: 'small' | 'medium' | 'large'; +} + +/** + * Primary UI component for user interaction + */ +export const NodeIcon = ({ iconId, status, size }: NodeIconProps) => { + let sizeStyle = mediumStyle; + if (size === 'small') { + sizeStyle = smallStyle; + } else if (size === 'large') { + sizeStyle = largeStyle; + } + let statusColorStyle; + if (status === 'healthy') { + statusColorStyle = healthy; + } else if (status === 'warning') { + statusColorStyle = warning; + } else if (status === 'error') { + statusColorStyle = error; + } else if (status === 'sync') { + statusColorStyle = sync; + } + let isStatusStyle; + if (status) { + isStatusStyle = hasStatusStyle; + } + + return ( +
+ {/* TODO: Replace image with CSS, and add pulsating effect */} + {status && ( +
+ )} +
+ Node icon +
+
+ ); +}; diff --git a/src/renderer/Generics/redesign/NodeIcon/nodeIcon.css.ts b/src/renderer/Generics/redesign/NodeIcon/nodeIcon.css.ts new file mode 100644 index 000000000..30776097a --- /dev/null +++ b/src/renderer/Generics/redesign/NodeIcon/nodeIcon.css.ts @@ -0,0 +1,134 @@ +import { style } from '@vanilla-extract/css'; +import { common, vars } from '../theme.css'; + +export const imageStyle = style({ + position: 'relative', + width: '100%', + height: '100%', + objectFit: 'contain', +}); + +export const hasStatusStyle = style({}); +export const smallStyle = style({}); +export const mediumStyle = style({}); +export const largeStyle = style({}); +export const healthy = style({ background: 'green' }); +export const warning = style({ background: 'yellow' }); +export const error = style({ background: 'red' }); +export const sync = style({ background: 'black' }); + +export const iconBackground = style({ + position: 'relative', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + boxSizing: 'border-box', + MozBoxSizing: 'border-box', + WebkitBoxSizing: 'border-box', + border: '1px solid rgba(0, 0, 2, 0.04)', + width: '100%', + height: '100%', + selectors: { + [`&.${smallStyle}`]: { + width: 32, + height: 32, + borderRadius: 12, + }, + [`&.${mediumStyle}`]: { + width: 40, + height: 40, + borderRadius: 14, + }, + [`&.${largeStyle}`]: { + width: 56, + height: 56, + borderRadius: 18, + }, + [`&.${smallStyle}.${hasStatusStyle}`]: { + WebkitMaskImage: + 'radial-gradient(circle 8px at calc(100% - 4px) calc(100% - 28px),transparent 6px,#000 0)', + }, + [`&.${mediumStyle}.${hasStatusStyle}`]: { + WebkitMaskImage: + 'radial-gradient(circle 10px at calc(100% - 5px) calc(100% - 35px),transparent 7px,#000 0)', + }, + [`&.${largeStyle}.${hasStatusStyle}`]: { + WebkitMaskImage: + 'radial-gradient( circle 26px at calc(100% - 7px) calc(100% - 49px),transparent 10px,#000 0)', + }, + }, +}); + +export const containerStyle = style({ + position: 'relative', + selectors: { + [`&.${smallStyle}`]: { + width: 32, + height: 32, + }, + [`&.${mediumStyle}`]: { + width: 40, + height: 40, + }, + [`&.${largeStyle}`]: { + width: 56, + height: 56, + }, + }, +}); + +export const iconStyle = style({ + position: 'relative', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + boxSizing: 'border-box', + MozBoxSizing: 'border-box', + WebkitBoxSizing: 'border-box', +}); + +export const statusStyle = style({ + boxSizing: 'border-box', + position: 'absolute', + right: '0', + top: '0', + zIndex: 1, + selectors: { + [`&.${smallStyle}`]: { + width: 8, + height: 8, + borderRadius: 4, + }, + [`&.${mediumStyle}`]: { + width: 10, + height: 10, + borderRadius: 5, + }, + [`&.${largeStyle}`]: { + width: 14, + height: 14, + borderRadius: 7, + }, + }, + // '&.darkMode': { '&.sync': { backgroundColor: 'rgba(255, 255, 255, 1)' } }, + // selectors: { + // '&.sync': { + // animation: 'rotation 2s infinite linear', + // right: '-1px', + // WebkitMaskSize: 'cover', + // maskSize: 'cover', + // backgroundColor: 'rgba(0, 0, 2, 0.95)', + // }, + // '&.small': { + // // '&.sync': { width: '10px', height: '8px', backgroundSize: '10px 8px' }, + // }, + // '&.medium': { + // // '&.sync': { width: '12px', height: '10px', backgroundSize: '12px 10px' }, + // }, + // '&.large': { + // // '&.sync': { width: '16px', height: '13px', backgroundSize: '16px 13px' }, + // }, + // }, +}); diff --git a/src/renderer/Generics/redesign/ProgressBar/TimedProgressBar.tsx b/src/renderer/Generics/redesign/ProgressBar/TimedProgressBar.tsx new file mode 100644 index 000000000..f2ddd27e4 --- /dev/null +++ b/src/renderer/Generics/redesign/ProgressBar/TimedProgressBar.tsx @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import ProgressBar, { ProgressBarProps } from './ProgressBar'; + +export interface TimedProgressBarProps extends ProgressBarProps { + totalTimeSeconds: number; +} + +const timeRemainingCaption = (totalTime: number, timeElapsed: number) => { + if (timeElapsed >= totalTime) { + return 'Finishing up...'; + } + return `About ${Math.round(totalTime - timeElapsed)} seconds remaining`; +}; + +const TimedProgressBar = ({ + totalTimeSeconds, + ...restProps +}: TimedProgressBarProps) => { + const [sElapsedSeconds, setElapsedSeconds] = useState(0); + + useEffect(() => { + const interval = setInterval(() => { + setElapsedSeconds((seconds) => seconds + 0.1); + }, 100); + return () => clearInterval(interval); + }, []); + + return ( + + ); +}; + +export default TimedProgressBar; diff --git a/src/renderer/Generics/redesign/RedesignContainer.tsx b/src/renderer/Generics/redesign/RedesignContainer.tsx index 80c962284..62a8b9be8 100644 --- a/src/renderer/Generics/redesign/RedesignContainer.tsx +++ b/src/renderer/Generics/redesign/RedesignContainer.tsx @@ -21,6 +21,7 @@ const RedesignContainerStoryBook: React.FC = ({ children }) => { marginTop: '1em', border: '1px dashed #E3E3E3', flexGrow: 1, + overflow: 'auto', /** * Then, because flex items cannot be smaller than the * size of their content – min-height: auto is the diff --git a/src/renderer/Generics/redesign/SelectCard/SelectCard.tsx b/src/renderer/Generics/redesign/SelectCard/SelectCard.tsx index 410d397dd..a1a1f235b 100644 --- a/src/renderer/Generics/redesign/SelectCard/SelectCard.tsx +++ b/src/renderer/Generics/redesign/SelectCard/SelectCard.tsx @@ -1,6 +1,6 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ import { NodeIconId } from 'renderer/assets/images/nodeIcons'; -import { NodeIcon } from '../NodeIcon'; +import { NodeIcon } from '../NodeIcon/NodeIcon'; import { Tag } from '../Tag'; import { container, diff --git a/src/renderer/Generics/redesign/SpecialSelect/SpecialSelect.tsx b/src/renderer/Generics/redesign/SpecialSelect/SpecialSelect.tsx index a7dede9fd..3c2a88ac2 100644 --- a/src/renderer/Generics/redesign/SpecialSelect/SpecialSelect.tsx +++ b/src/renderer/Generics/redesign/SpecialSelect/SpecialSelect.tsx @@ -1,6 +1,7 @@ /* eslint-disable react/destructuring-assignment */ // Options replaceable component docs: // https://react-select.com/components#Option +import { useEffect, useState } from 'react'; import Select, { OptionProps, ValueContainerProps } from 'react-select'; import SelectCard from '../SelectCard/SelectCard'; import { vars } from '../theme.css'; @@ -19,71 +20,56 @@ const SingleValue = ({ children, ...props }: ValueContainerProps) => { return (
- - {/* {`${nething[0]?.label} ${nething[0]?.value}`} */}
); }; - -const ecOptions = [ - { - iconId: 'geth', - value: 'geth', - label: 'Geth', - title: 'Geth', - info: 'Execution Client', - onClick() { - console.log('hello'); - }, - }, - { - iconId: 'erigon', - value: 'erigon', - label: 'Erigon', - title: 'Erigon', - info: 'Execution Client', - }, - { - iconId: 'nethermind', - value: 'nethermind', - label: 'Nethermind', - title: 'Nethermind', - info: 'Execution Client', - }, - { - iconId: 'besu', - value: 'besu', - label: 'Besu', - title: 'Besu', - info: 'Execution Client', - minority: true, - onClick() { - console.log('hello'); - }, - }, -]; -// const options = [ -// { value: 'lodestar', label: 'lodestar', storage: 100, minory: true }, -// { value: 'prysm', label: 'prysm', storage: 1000 }, -// { value: 'teku', label: 'teku', minory: true, storage: 9009 }, -// { value: 'lighthouse', label: 'lighthouse', storage: 1 }, -// { value: 'nimbus', label: 'nimbus', storage: 69 }, -// ]; export interface SpecialSelectProps { options?: any[]; - onChange?: (newValue: string) => void; + onChange?: (newValue: any) => void; } /** - * Use for selecting Ethereum node client + * Used for selecting Ethereum node client */ const SpecialSelect = ({ options, onChange, ...props }: SpecialSelectProps) => { + const [sSelectedOption, setSelectedOption] = useState(); + + useEffect(() => { + // if (onChange && options && options[0]) { + // todo: fix, may call multiple times + console.log('useEffect(sSelectedOption, options): '); + + if (!sSelectedOption && options && options[0]) { + setSelectedOption(options[0]); + } + // } + }, [sSelectedOption, options]); + + useEffect(() => { + // if (onChange && options && options[0]) { + // todo: fix, may call multiple times + console.log('useEffect(sSelectedOption, onChange): '); + + if (onChange) { + onChange(sSelectedOption); + } + // } + }, [sSelectedOption, onChange]); + + const onSelectChange = (newValue: any) => { + console.log('onSelectChange: ', newValue); + setSelectedOption(newValue); + }; + return ( <>