diff --git a/package-lock.json b/package-lock.json index 1808f4f4d..4dbd0a3a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nice-node", - "version": "1.5.0-alpha", + "version": "2.0.0-alpha", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "nice-node", - "version": "1.5.0-alpha", + "version": "2.0.0-alpha", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -42,7 +42,7 @@ "react-select": "^5.4.0", "react-tooltip": "^4.2.21", "styled-components": "^5.3.5", - "systeminformation": "^5.12.6", + "systeminformation": "^5.12.11", "throttle-debounce": "^5.0.0", "uuid": "^8.3.2", "winston": "^3.7.2", @@ -31755,9 +31755,9 @@ "dev": true }, "node_modules/systeminformation": { - "version": "5.12.6", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.12.6.tgz", - "integrity": "sha512-FkCvT5BOuH1OE3+8lFM25oXIYJ0CM8kq4Wgvz2jyBTrsOIgha/6gdJXgbF4rv+g0j/5wJqQLDKan7kc/p7uIvw==", + "version": "5.12.11", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.12.11.tgz", + "integrity": "sha512-4N5nT4BFWqRyadTLO8c/t8/gM6wqgg26/WNjjZCS/UU7VuURuBy/pR6Z6+j0nD3ff+zCpX/sdVfyn+EoIg9saQ==", "os": [ "darwin", "linux", @@ -59154,9 +59154,9 @@ "dev": true }, "systeminformation": { - "version": "5.12.6", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.12.6.tgz", - "integrity": "sha512-FkCvT5BOuH1OE3+8lFM25oXIYJ0CM8kq4Wgvz2jyBTrsOIgha/6gdJXgbF4rv+g0j/5wJqQLDKan7kc/p7uIvw==" + "version": "5.12.11", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.12.11.tgz", + "integrity": "sha512-4N5nT4BFWqRyadTLO8c/t8/gM6wqgg26/WNjjZCS/UU7VuURuBy/pR6Z6+j0nD3ff+zCpX/sdVfyn+EoIg9saQ==" }, "tapable": { "version": "2.2.1", diff --git a/package.json b/package.json index 1ac3a6846..0d4c87e1b 100644 --- a/package.json +++ b/package.json @@ -306,7 +306,7 @@ "react-select": "^5.4.0", "react-tooltip": "^4.2.21", "styled-components": "^5.3.5", - "systeminformation": "^5.12.6", + "systeminformation": "^5.12.11", "throttle-debounce": "^5.0.0", "uuid": "^8.3.2", "winston": "^3.7.2", diff --git a/src/common/NodeSpecs/geth/geth-v1.0.0.json b/src/common/NodeSpecs/geth/geth-v1.0.0.json index f544b62b9..04a8a272c 100644 --- a/src/common/NodeSpecs/geth/geth-v1.0.0.json +++ b/src/common/NodeSpecs/geth/geth-v1.0.0.json @@ -42,7 +42,7 @@ "minSizeGBs": 16 }, "storage" : { - "minSizeGBs" : 2000, + "minSizeGBs" : 1600, "ssdRequired": true }, "internet" : { diff --git a/src/main/dialog.ts b/src/main/dialog.ts index b0e85324e..342979c6d 100644 --- a/src/main/dialog.ts +++ b/src/main/dialog.ts @@ -1,7 +1,11 @@ import { BrowserWindow, dialog } from 'electron'; import { NodeId } from '../common/node'; -import { getNodesDirPath } from './files'; +import { + getNodesDirPath, + CheckStorageDetails, + getSystemFreeDiskSpace, +} from './files'; import logger from './logger'; // eslint-disable-next-line import/no-cycle import { getMainWindow } from './main'; @@ -46,3 +50,36 @@ export const openDialogForNodeDataDir = async (nodeId: NodeId) => { } } }; + +export const openDialogForStorageLocation = async (): Promise< + CheckStorageDetails | undefined +> => { + const mainWindow: BrowserWindow | null = getMainWindow(); + if (!mainWindow) { + logger.error( + 'Unable to open dialog to select storage location. mainWindow is null.' + ); + return; + } + const defaultPath = getNodesDirPath(); + const result = await dialog.showOpenDialog(mainWindow, { + title: `Select a folder for storing node data`, + defaultPath, + properties: ['openDirectory'], + }); + console.log('dir select result: ', result); + if (result.canceled) { + return; + } + if (result.filePaths) { + if (result.filePaths.length > 0) { + const folderPath = result.filePaths[0]; + const freeStorageGBs = await getSystemFreeDiskSpace(folderPath); + // eslint-disable-next-line consistent-return + return { + folderPath, + freeStorageGBs, + }; + } + } +}; diff --git a/src/main/files.ts b/src/main/files.ts index ac0079db3..5b5b08b6a 100644 --- a/src/main/files.ts +++ b/src/main/files.ts @@ -55,11 +55,15 @@ export const doesFileOrDirExist = async ( /** * - * @returns checkOrMakeNodeDir at getNodesDirPath() + nodeDirName + * @returns checkOrMakeNodeDir at storageLocation + nodeDirName + * or getNodesDirPath() + nodeDirName * @throws error if it cannot make the directory */ -export const makeNodeDir = async (nodeDirName: string): Promise => { - const nodeDir = path.join(getNodesDirPath(), nodeDirName); +export const makeNodeDir = async ( + nodeDirName: string, + storageLocation?: string +): Promise => { + const nodeDir = path.join(storageLocation ?? getNodesDirPath(), nodeDirName); await checkAndOrCreateDir(nodeDir); return nodeDir; }; @@ -68,11 +72,36 @@ export const gethDataDir = (): string => { return `${getNNDirPath()}/geth-mainnet`; }; -export const getSystemFreeDiskSpace = async (): Promise => { - const diskSpace = await checkDiskSpace(app.getPath('userData')); +/** + * @param diskSpacePath fold path to check free disk space at. Helpful for checking + * free space on different storage devices. + * @returns (GBs) free storage space + */ +export const getSystemFreeDiskSpace = async ( + diskSpacePath?: string +): Promise => { + const pathToCheck: string = diskSpacePath || app.getPath('userData'); + console.log('pathToCheck', pathToCheck); + const diskSpace = await checkDiskSpace(pathToCheck); const freeInGBs = diskSpace.free * 1e-9; return freeInGBs; }; + +export type CheckStorageDetails = { + folderPath: string; + freeStorageGBs: number; +}; + +export const getNodesDirPathDetails = + async (): Promise => { + const folderPath = getNodesDirPath(); + const freeStorageGBs = await getSystemFreeDiskSpace(folderPath); + return { + folderPath, + freeStorageGBs, + }; + }; + export const getSystemDiskSize = async (): Promise => { const diskSpace = await checkDiskSpace(app.getPath('userData')); const sizeInGBs = diskSpace.size * 1e-9; diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 2f6e57084..3c7e04113 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,7 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ipcMain } from 'electron'; import getDebugInfo from './debug'; -import { getGethLogs, getGethErrorLogs, getSystemFreeDiskSpace } from './files'; +import { + getGethLogs, + getGethErrorLogs, + getSystemFreeDiskSpace, + getNodesDirPath, + getNodesDirPathDetails, +} from './files'; import store from './state/store'; import logger from './logger'; import { @@ -24,7 +30,10 @@ import { NodeSpecification } from '../common/nodeSpec'; import { isDockerInstalled, isDockerRunning } from './docker/docker'; import installDocker from './docker/install'; // eslint-disable-next-line import/no-cycle -import { openDialogForNodeDataDir } from './dialog'; +import { + openDialogForNodeDataDir, + openDialogForStorageLocation, +} from './dialog'; import { getNodeLibrary } from './state/nodeLibrary'; import { getSettings, setLanguage } from './state/settings'; import { getSystemInfo } from './systemInfo'; @@ -35,7 +44,9 @@ export const initialize = () => { ipcMain.handle('updateNodeUsedDiskSpace', (_event, nodeId: NodeId) => { return updateNodeUsedDiskSpace(nodeId); }); - ipcMain.handle('getSystemFreeDiskSpace', getSystemFreeDiskSpace); + ipcMain.handle('getSystemFreeDiskSpace', (_event) => { + return getSystemFreeDiskSpace(); + }); ipcMain.handle('getDebugInfo', getDebugInfo); ipcMain.handle('getStoreValue', (_event, key: string) => { const value = store.get(key); @@ -56,9 +67,12 @@ export const initialize = () => { // Multi-nodegetUserNodes ipcMain.handle('getNodes', getNodes); ipcMain.handle('getUserNodes', getUserNodes); - ipcMain.handle('addNode', (_event, nodeSpec: NodeSpecification) => { - return addNode(nodeSpec); - }); + ipcMain.handle( + 'addNode', + (_event, nodeSpec: NodeSpecification, storageLocation?: string) => { + return addNode(nodeSpec, storageLocation); + } + ); ipcMain.handle( 'updateNode', (_event, nodeId: NodeId, propertiesToUpdate: any) => { @@ -80,6 +94,9 @@ export const initialize = () => { ipcMain.handle('openDialogForNodeDataDir', (_event, nodeId: NodeId) => { return openDialogForNodeDataDir(nodeId); }); + ipcMain.handle('openDialogForStorageLocation', () => { + return openDialogForStorageLocation(); + }); ipcMain.handle('deleteNodeStorage', (_event, nodeId: NodeId) => { return deleteNodeStorage(nodeId); }); @@ -90,6 +107,9 @@ export const initialize = () => { return stopSendingNodeLogs(nodeId); }); + // Default Node storage location + ipcMain.handle('getNodesDefaultStorageLocation', getNodesDirPathDetails); + // Node library ipcMain.handle('getNodeLibrary', getNodeLibrary); diff --git a/src/main/nodeManager.ts b/src/main/nodeManager.ts index 9c2598611..46758f6df 100644 --- a/src/main/nodeManager.ts +++ b/src/main/nodeManager.ts @@ -18,7 +18,7 @@ import Node, { NodeStatus, } from '../common/node'; import * as nodeStore from './state/nodes'; -import { deleteDisk, makeNodeDir } from './files'; +import { deleteDisk, getNodesDirPath, makeNodeDir } from './files'; import { startBinary, stopBinary, @@ -32,8 +32,17 @@ import { } from './binary'; import { initialize as initNodeLibrary } from './nodeLibraryManager'; -export const addNode = async (nodeSpec: NodeSpecification): Promise => { - const dataDir = await makeNodeDir(nodeSpec.specId); +export const addNode = async ( + nodeSpec: NodeSpecification, + storageLocation?: string +): Promise => { + // use a timestamp postfix so the user can add multiple nodes of the same name + const utcTimestamp = Math.floor(Date.now() / 1000); + const dataDir = await makeNodeDir( + `${nodeSpec.specId}-${utcTimestamp}`, + storageLocation ?? getNodesDirPath() + ); + console.log('adding node with dataDir: ', dataDir); const nodeRuntime: NodeRuntime = { dataDir, usage: {}, diff --git a/src/main/preload.ts b/src/main/preload.ts index 13dbf4542..4d933019e 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -48,8 +48,8 @@ contextBridge.exposeInMainWorld('electron', { // Multi-node getNodes: () => ipcRenderer.invoke('getNodes'), getUserNodes: () => ipcRenderer.invoke('getUserNodes'), - addNode: (nodeSpec: NodeSpecification) => - ipcRenderer.invoke('addNode', nodeSpec), + addNode: (nodeSpec: NodeSpecification, storageLocation?: string) => + ipcRenderer.invoke('addNode', nodeSpec, storageLocation), updateNode: (nodeId: NodeId, propertiesToUpdate: any) => ipcRenderer.invoke('updateNode', nodeId, propertiesToUpdate), @@ -64,6 +64,8 @@ contextBridge.exposeInMainWorld('electron', { }, openDialogForNodeDataDir: (nodeId: NodeId) => ipcRenderer.invoke('openDialogForNodeDataDir', nodeId), + openDialogForStorageLocation: () => + ipcRenderer.invoke('openDialogForStorageLocation'), deleteNodeStorage: (nodeId: NodeId) => ipcRenderer.invoke('deleteNodeStorage', nodeId), sendNodeLogs: (nodeId: NodeId) => { @@ -73,6 +75,10 @@ contextBridge.exposeInMainWorld('electron', { ipcRenderer.invoke('stopSendingNodeLogs', nodeId); }, + // Default Node storage location + getNodesDefaultStorageLocation: () => + ipcRenderer.invoke('getNodesDefaultStorageLocation'), + // Node library getNodeLibrary: () => ipcRenderer.invoke('getNodeLibrary'), diff --git a/src/main/systemInfo.ts b/src/main/systemInfo.ts index d02e106f6..5f209007d 100644 --- a/src/main/systemInfo.ts +++ b/src/main/systemInfo.ts @@ -1,6 +1,10 @@ import si from 'systeminformation'; -export type SystemData = si.Systeminformation.StaticData; +export type SystemData = si.Systeminformation.StaticData & { + blockDevices: si.Systeminformation.BlockDevicesData[]; + fsSize: si.Systeminformation.FsSizeData[]; +}; + // const getCpuInfo = async (): Promise => { // const data = await si.cpu(); // console.log('Cpu data: ', data); @@ -54,20 +58,23 @@ export type SystemData = si.Systeminformation.StaticData; // return data; // }; -// const getAllStaticInfo = async (): Promise => { -// const data = await si.getStaticData(); -// console.log('getStaticData data: ', data); -// return data; -// }; - /** * Returns detailed system information and sends async info * over a channel to the UI when it is determined. */ export const getSystemInfo = async (): Promise => { - const data = await si.getStaticData(); - console.log('getStaticData data: ', JSON.stringify(data, null, 4)); - return data; + const staticData = await si.getStaticData(); + // ); + const blockDevices = await si.blockDevices(); + const fsSize = await si.fsSize(); + const systemData = { + ...staticData, + blockDevices, + fsSize, + }; + console.log('systemData data: ', JSON.stringify(staticData, null, 4)); + + return systemData; // start monitoring sys_usage? // getCpuInfo(); // getMemInfo(); @@ -81,6 +88,10 @@ export const getSystemInfo = async (): Promise => { }; export const initialize = async () => { - // start monitoring sys_usage? + // Currently used for debugging. + // Also, it is good to "warm-up" Windows. + // "Especially on Windows this can take really long (up to 20 seconds) + // \ because the underlying get-WmiObject command is very slow when + // \ using it the first time." getSystemInfo(); }; diff --git a/src/renderer/Generics/redesign/Input/FolderInput.tsx b/src/renderer/Generics/redesign/Input/FolderInput.tsx new file mode 100644 index 000000000..52e0fdfd5 --- /dev/null +++ b/src/renderer/Generics/redesign/Input/FolderInput.tsx @@ -0,0 +1,51 @@ +import Button from '../Button/Button'; +import { + container, + pathAndChangeContainer, + freeStorageSpaceFontStyle, +} from './folderInput.css'; +import Input from './Input'; + +export interface FolderInputProps { + /** + * The folder path + */ + placeholder?: string; + /** + * Free storage space of the storage device where the folder + * is located (mounted). + */ + freeStorageSpaceGBs?: number; + /** + * Provide to allow the user to change the folder location + */ + onClickChange?: () => void; +} + +const FolderInput = ({ + placeholder, + freeStorageSpaceGBs, + onClickChange, +}: FolderInputProps) => { + return ( +
+
+
+ +
+
+ {freeStorageSpaceGBs && ( + + {Math.round(freeStorageSpaceGBs)}GB available storage + + )} +
+ ); +}; +export default FolderInput; diff --git a/src/renderer/Generics/redesign/Input/folderInput.css.ts b/src/renderer/Generics/redesign/Input/folderInput.css.ts new file mode 100644 index 000000000..18596b792 --- /dev/null +++ b/src/renderer/Generics/redesign/Input/folderInput.css.ts @@ -0,0 +1,24 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '../theme.css'; + +export const container = style({ + display: 'flex', + flexDirection: 'column', + gap: 8, + width: '100%', +}); + +export const pathAndChangeContainer = style({ + boxSizing: 'border-box', + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + padding: '0px', + gap: 20, +}); + +export const freeStorageSpaceFontStyle = style({ + fontSize: 11, + lineHeight: '14px', + color: vars.color.font50, +}); diff --git a/src/renderer/Presentational/AddEthereumNode/AddEthereumNode.tsx b/src/renderer/Presentational/AddEthereumNode/AddEthereumNode.tsx index f0b60ec90..fcb8dc670 100644 --- a/src/renderer/Presentational/AddEthereumNode/AddEthereumNode.tsx +++ b/src/renderer/Presentational/AddEthereumNode/AddEthereumNode.tsx @@ -16,6 +16,8 @@ import Input from '../../Generics/redesign/Input/Input'; import DropdownLink from '../../Generics/redesign/Link/DropdownLink'; import Select from '../../Generics/redesign/Select/Select'; import { NodeSpecification } from '../../../common/nodeSpec'; +import FolderInput from '../../Generics/redesign/Input/FolderInput'; +import { HorizontalLine } from '../../Generics/redesign/HorizontalLine/HorizontalLine'; const ecOptions = [ { @@ -92,9 +94,10 @@ const ccOptions = [ }, ]; -type AddEthereumNodeValues = { +export type AddEthereumNodeValues = { executionClient?: string; consensusClient?: string; + storageLocation?: string; }; export interface AddEthereumNodeProps { executionOptions: NodeSpecification[]; @@ -107,6 +110,9 @@ export interface AddEthereumNodeProps { const AddEthereumNode = ({ onChange, + /** + * Todo: Pass options from the node spec files + */ executionOptions, beaconOptions, }: AddEthereumNodeProps) => { @@ -116,36 +122,24 @@ const AddEthereumNode = ({ useState(); const [sSelectedConsensusClient, setSelectedConsensusClient] = useState(); - // const [sExecutionOptions, setExecutionOptions] = useState(); - // const [sBeaconOptions, setBeaconOptions] = useState(); - - // useEffect(() => { - // const formattedExecutionOptions: any[] = []; - // executionOptions.forEach((opt) => { - // const formattedOption = { - // ...opt, - // label: opt.displayName, - // value: opt.specId, - // }; - // formattedExecutionOptions.push(formattedOption); - // }); - // setExecutionOptions(formattedExecutionOptions); - // }, [executionOptions]); + const [sNodeStorageLocation, setNodeStorageLocation] = useState(); + const [ + sNodeStorageLocationFreeStorageGBs, + setNodeStorageLocationFreeStorageGBs, + ] = useState(); - // useEffect(() => { - // const formattedBeaconOptions: any[] = []; - // beaconOptions.forEach((opt) => { - // const formattedOption = { - // ...opt, - // label: opt.displayName, - // value: opt.specId, - // }; - // formattedBeaconOptions.push(formattedOption); - // }); - // setBeaconOptions(formattedBeaconOptions); - // }, [beaconOptions]); - // on change client or setting, return NodeSpecIds, Node Settings, and storage location - // NodeSpecs are only req'd in parent component until Node Settings + useEffect(() => { + const fetchData = async () => { + const defaultNodesStorageDetails = + await electron.getNodesDefaultStorageLocation(); + console.log('defaultNodesStorageDetails', defaultNodesStorageDetails); + setNodeStorageLocation(defaultNodesStorageDetails.folderPath); + setNodeStorageLocationFreeStorageGBs( + defaultNodesStorageDetails.freeStorageGBs + ); + }; + fetchData(); + }, []); const onChangeEc = useCallback((newEc: any) => { console.log('new selected execution client: ', newEc); @@ -161,9 +155,15 @@ const AddEthereumNode = ({ onChange({ executionClient: sSelectedExecutionClient, consensusClient: sSelectedConsensusClient, + storageLocation: sNodeStorageLocation, }); } - }, [sSelectedExecutionClient, sSelectedConsensusClient, onChange]); + }, [ + sSelectedExecutionClient, + sSelectedConsensusClient, + sNodeStorageLocation, + onChange, + ]); return (
@@ -179,7 +179,6 @@ const AddEthereumNode = ({

Consensus client

-

Data location

setIsOptionsOpen(!sIsOptionsOpen)} @@ -202,26 +201,25 @@ const AddEthereumNode = ({
)} -
+

Data location

+ { + const storageLocationDetails = + await electron.openDialogForStorageLocation(); + console.log('storageLocationDetails', storageLocationDetails); + if (storageLocationDetails) { + setNodeStorageLocation(storageLocationDetails.folderPath); + setNodeStorageLocationFreeStorageGBs( + storageLocationDetails.freeStorageGBs + ); + } else { + // user didn't change the folder path + } }} - > -
- -
-
+ /> ); }; diff --git a/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx b/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx index bc6072cf5..58fee412e 100644 --- a/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx +++ b/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx @@ -4,7 +4,9 @@ import { useCallback, useEffect, useState } from 'react'; import { componentContainer, container } from './addNodeStepper.css'; import Stepper from '../../Generics/redesign/Stepper/Stepper'; -import AddEthereumNode from '../AddEthereumNode/AddEthereumNode'; +import AddEthereumNode, { + AddEthereumNodeValues, +} from '../AddEthereumNode/AddEthereumNode'; import DockerInstallation from '../DockerInstallation/DockerInstallation'; import NodeRequirements from '../NodeRequirements/NodeRequirements'; import electron from '../../electronGlobal'; @@ -15,6 +17,7 @@ import { SystemData } from '../../../main/systemInfo'; import { mergeSystemRequirements } from './mergeNodeRequirements'; import { updateSelectedNodeId } from '../../state/node'; import { useAppDispatch } from '../../state/hooks'; +import { CheckStorageDetails } from '../../../main/files'; export interface AddNodeStepperProps { onChange: (newValue: 'done' | 'cancel') => void; @@ -37,11 +40,12 @@ const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => { const [sEthereumNodeConfig, setEthereumNodeConfig] = useState(); const [sEthereumNodeRequirements, setEthereumNodeRequirements] = useState(); + const [sNodeStorageLocation, setNodeStorageLocation] = useState(); - const [sData, setData] = useState(); + const [sSystemData, setSystemData] = useState(); const getData = async () => { - setData(await electron.getSystemInfo()); + setSystemData(await electron.getSystemInfo()); }; useEffect(() => { @@ -72,7 +76,7 @@ const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => { }, []); const onChangeAddEthereumNode = useCallback( - (newValue: any) => { + (newValue: AddEthereumNodeValues) => { console.log('onChangeAddEthereumNode newValue ', newValue); setEthereumNodeConfig(newValue); let ecReqs; @@ -96,6 +100,9 @@ const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => { } catch (e) { console.error(e); } + + // save storage location (and other settings) + setNodeStorageLocation(newValue.storageLocation); }, [sNodeLibrary] ); @@ -121,9 +128,13 @@ const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => { ccNodeSpec = sNodeLibrary?.[`${ccValue}-beacon`]; } } - const ecNode = await electron.addNode(ecNodeSpec); + console.log( + 'adding nodes with storage location set to: ', + sNodeStorageLocation + ); + const ecNode = await electron.addNode(ecNodeSpec, sNodeStorageLocation); console.log('addNode returned node: ', ecNode); - const ccNode = await electron.addNode(ccNodeSpec); + const ccNode = await electron.addNode(ccNodeSpec, sNodeStorageLocation); console.log('addNode returned node: ', ccNode); dispatch(updateSelectedNodeId(ecNode.id)); const startEcResult = await electron.startNode(ecNode.id); @@ -175,7 +186,8 @@ const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => {
diff --git a/src/renderer/Presentational/NodeRequirements/NodeRequirements.tsx b/src/renderer/Presentational/NodeRequirements/NodeRequirements.tsx index 1744718e3..28d99975c 100644 --- a/src/renderer/Presentational/NodeRequirements/NodeRequirements.tsx +++ b/src/renderer/Presentational/NodeRequirements/NodeRequirements.tsx @@ -19,11 +19,16 @@ export interface NodeRequirementsProps { * Title of the checklist */ systemData?: SystemData; + /** + * A folder path where the node data will be stored. + */ + nodeStorageLocation?: string; } const NodeRequirements = ({ nodeRequirements, systemData, + nodeStorageLocation, }: NodeRequirementsProps) => { const { t } = useTranslation('systemRequirements'); const [sItems, setItems] = useState([]); @@ -42,11 +47,12 @@ const NodeRequirements = ({ { nodeRequirements, systemData, + nodeStorageLocation, }, t ); setItems(newChecklistItems); - }, [nodeRequirements, systemData, t]); + }, [nodeRequirements, systemData, nodeStorageLocation, t]); return (
diff --git a/src/renderer/Presentational/NodeRequirements/nodeStorageUtil.ts b/src/renderer/Presentational/NodeRequirements/nodeStorageUtil.ts new file mode 100644 index 000000000..751dd6622 --- /dev/null +++ b/src/renderer/Presentational/NodeRequirements/nodeStorageUtil.ts @@ -0,0 +1,61 @@ +import { Systeminformation } from 'systeminformation'; +import { SystemData } from '../../../main/systemInfo'; + +export type SystemStorageLocation = { + type: string; + name: string; + freeSpaceGBs: number; +}; + +/** + * The mounted storage partition and storage device are found by + * matching the location to one of the storage mounts in systemData + * @param systemData + * @param nodeStorageLocation + */ +export const findSystemStorageDetailsAtALocation = ( + systemData: SystemData, + nodeStorageLocation: string +): SystemStorageLocation | undefined => { + // From blockDevices…If( storageLoc.startsWith(mountPath) && highest Char match) + // ignore mountPath “/“ case, if no match, assume? + + if (!systemData) { + return undefined; + } + + let longestMountPathMatch = -1; + let longestMatchBlockDevice: Systeminformation.BlockDevicesData | undefined; + systemData.blockDevices?.forEach((blockDevice) => { + if (nodeStorageLocation.startsWith(blockDevice.mount)) { + if (blockDevice.mount.length > longestMountPathMatch) { + longestMountPathMatch = blockDevice.mount.length; + longestMatchBlockDevice = blockDevice; + } + } + }); + + if (longestMatchBlockDevice === undefined) { + throw new Error( + `No storage device found for location ${nodeStorageLocation}` + ); + } + + // Find free storage + let matchedFileSystemSizes: Systeminformation.FsSizeData | undefined; + systemData.fsSize?.forEach((fileSystemSizes) => { + if (longestMatchBlockDevice?.mount === fileSystemSizes.mount) { + matchedFileSystemSizes = fileSystemSizes; + } + }); + + if (matchedFileSystemSizes === undefined) { + throw new Error(`No filesystem found for location ${nodeStorageLocation}`); + } + + return { + type: longestMatchBlockDevice.physical, + name: longestMatchBlockDevice.label, + freeSpaceGBs: matchedFileSystemSizes.available * 1e-9, + }; +}; diff --git a/src/renderer/Presentational/NodeRequirements/requirementsChecklistUtil.tsx b/src/renderer/Presentational/NodeRequirements/requirementsChecklistUtil.tsx index d4b35cc5b..6549a1302 100644 --- a/src/renderer/Presentational/NodeRequirements/requirementsChecklistUtil.tsx +++ b/src/renderer/Presentational/NodeRequirements/requirementsChecklistUtil.tsx @@ -13,15 +13,26 @@ import ExternalLink from '../../Generics/redesign/Link/ExternalLink'; import { bytesToGB } from '../../utils'; // eslint-disable-next-line import/no-cycle import { NodeRequirementsProps } from './NodeRequirements'; +import { findSystemStorageDetailsAtALocation } from './nodeStorageUtil'; export const makeCheckList = ( - { nodeRequirements, systemData }: NodeRequirementsProps, + { nodeRequirements, systemData, nodeStorageLocation }: NodeRequirementsProps, t: TFunction<'systemRequirements', undefined> ) => { const newChecklistItems: ChecklistItemProps[] = []; if (!nodeRequirements) { return newChecklistItems; } + + let nodeLocationStorageDetails; + if (systemData && nodeStorageLocation) { + nodeLocationStorageDetails = findSystemStorageDetailsAtALocation( + systemData, + nodeStorageLocation + ); + } + console.log('nodeLocationStorageDetails', nodeLocationStorageDetails); + // eslint-disable-next-line no-restricted-syntax for (const [nodeReqKey, nodeReqValue] of Object.entries(nodeRequirements)) { console.log(`${nodeReqKey}: ${nodeReqValue}`); @@ -80,7 +91,7 @@ export const makeCheckList = ( } if (nodeReqKey === 'storage') { const req = nodeReqValue as StorageRequirements; - const disk = systemData?.diskLayout[0]; + const disk = nodeLocationStorageDetails; if (req.ssdRequired === true) { checkTitle = t('storageTypeTitle', { type: 'SSD', @@ -121,8 +132,8 @@ export const makeCheckList = ( checkTitle = t('storageSizeTitle', { minSize: req.minSizeGBs, }); - if (disk?.size) { - const diskSizeGbs = bytesToGB(disk.size); + if (disk?.freeSpaceGBs) { + const diskSizeGbs = Math.round(disk.freeSpaceGBs); // todo: use free space for storage calculations? valueText = t('storageSizeDescription', { freeSize: diskSizeGbs, diff --git a/src/renderer/__mocks__/custom-preload-mock.ts b/src/renderer/__mocks__/custom-preload-mock.ts index 8fdfde9fe..8f31df048 100644 --- a/src/renderer/__mocks__/custom-preload-mock.ts +++ b/src/renderer/__mocks__/custom-preload-mock.ts @@ -63,6 +63,7 @@ export const removeNode = () => {}; export const startNode = () => {}; export const stopNode = () => {}; export const openDialogForNodeDataDir = () => {}; +export const openDialogForStorageLocation = () => {}; export const updateNodeUsedDiskSpace = () => {}; export const deleteNodeStorage = () => true; export const sendNodeLogs = () => {}; diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index 7ba24f8fd..74a6fb013 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -5,6 +5,8 @@ import { NodeSpecification } from '../common/nodeSpec'; import { Node, NodeId } from '../common/node'; import { NodeLibrary } from '../main/state/nodeLibrary'; import { Settings } from '../main/state/settings'; +import { CheckStorageDetails } from '../main/files'; +import { SystemData } from '../main/systemInfo'; // Since we are using Chrome only in Electron and this is not a web standard yet, // we extend window.performance to include Chrome's memory stats @@ -41,22 +43,26 @@ declare global { getRendererProcessUsage(): any; getMainProcessUsage(): any; checkSystemHardware(): string[]; - getSystemInfo(): si.Systeminformation.StaticData; + getSystemInfo(): SystemData; // Multi-node getNodes(): Node[]; getUserNodes(): UserNodes; - addNode(nodeSpec: NodeSpecification): Node; + addNode(nodeSpec: NodeSpecification, storageLocation?: string): Node; updateNode(nodeId: NodeId, propertiesToUpdate: any): Node; removeNode(nodeId: NodeId, options: { isDeleteStorage: boolean }): Node; startNode(nodeId: NodeId): void; stopNode(nodeId: NodeId): void; openDialogForNodeDataDir(nodeId: NodeId): void; + openDialogForStorageLocation(): CheckStorageDetails; updateNodeUsedDiskSpace(nodeId: NodeId): void; deleteNodeStorage(nodeId: NodeId): boolean; sendNodeLogs(nodeId: NodeId): void; stopSendingNodeLogs(nodeId?: NodeId): void; + // Default Node storage location + getNodesDefaultStorageLocation(): CheckStorageDetails; + // Node library getNodeLibrary(): NodeLibrary; diff --git a/src/stories/Generic/Input.stories.tsx b/src/stories/Generic/Input.stories.tsx index a2fd65202..08603e0aa 100644 --- a/src/stories/Generic/Input.stories.tsx +++ b/src/stories/Generic/Input.stories.tsx @@ -1,4 +1,5 @@ import { ComponentStory, ComponentMeta } from '@storybook/react'; +import FolderInput from '../../renderer/Generics/redesign/Input/FolderInput'; import Input from '../../renderer/Generics/redesign/Input/Input'; @@ -21,3 +22,16 @@ export const Primary = Template.bind({}); Primary.args = { placeholder: 'Test placeholder', }; + +const FolderTemplate: ComponentStory = (args) => ( + +); + +export let FolderInputPrimary = FolderTemplate.bind({}); +FolderInputPrimary.args = { + placeholder: '/Users/Danneh/Library/Application Library/NiceNode/nodes', + onClickChange: () => { + alert('Platform specific user prompt to select a folder location') + }, + freeStorageSpaceMBs: 250 +};