diff --git a/src/main/consts/notifications.ts b/src/main/consts/notifications.ts new file mode 100644 index 000000000..6ffd1718e --- /dev/null +++ b/src/main/consts/notifications.ts @@ -0,0 +1,24 @@ +export const NOTIFICATIONS = Object.freeze({ + info: { + SYNC_COMMITTEE: { + title: 'Scheduled for Sync Commitee Duty', + description: 'Validator', + }, + SLASH_REWARD: { + title: 'Reward for slashing another validator', + description: 'Validator', + }, + }, + completed: { + CLIENT_UPDATED: { + title: 'Client successfuly updated', + description: 'consensus client', + }, + }, + download: { + UPDATE_AVAILABLE: { + title: 'Client successfuly updated', + description: 'consensus client', + }, + }, +}); diff --git a/src/main/dialog.ts b/src/main/dialog.ts index 731d539a9..742914d66 100644 --- a/src/main/dialog.ts +++ b/src/main/dialog.ts @@ -1,6 +1,7 @@ +/* eslint-disable consistent-return */ import { BrowserWindow, dialog } from 'electron'; -import { NodeId } from '../common/node'; +import Node, { NodeId } from '../common/node'; import { getNodesDirPath, CheckStorageDetails, @@ -11,6 +12,12 @@ import logger from './logger'; import { getMainWindow } from './main'; import { getNode, updateNode } from './state/nodes'; +export const updateNodeDataDir = async (node: Node, newDataDir: string) => { + node.runtime.dataDir = newDataDir; + node.config.configValuesMap.dataDir = newDataDir; + updateNode(node); +}; + export const openDialogForNodeDataDir = async (nodeId: NodeId) => { const node = getNode(nodeId); if (!node) { @@ -43,12 +50,11 @@ export const openDialogForNodeDataDir = async (nodeId: NodeId) => { } if (result.filePaths) { if (result.filePaths.length > 0) { - const newDataDir = result.filePaths[0]; - node.runtime.dataDir = newDataDir; - node.config.configValuesMap.dataDir = newDataDir; - updateNode(node); + return result.filePaths[0]; } } + // eslint-disable-next-line no-useless-return + return; }; export const openDialogForStorageLocation = async (): Promise< @@ -75,7 +81,6 @@ export const openDialogForStorageLocation = async (): Promise< 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/ipc.ts b/src/main/ipc.ts index 269b50f82..97687eb67 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -33,6 +33,7 @@ import installDocker from './docker/install'; import { openDialogForNodeDataDir, openDialogForStorageLocation, + updateNodeDataDir, } from './dialog'; import { getNodeLibrary } from './state/nodeLibrary'; import { @@ -113,6 +114,12 @@ export const initialize = () => { ipcMain.handle('stopNode', (_event, nodeId: NodeId) => { return stopNode(nodeId); }); + ipcMain.handle( + 'updateNodeDataDir', + (_event, node: Node, newDataDir: string) => { + return updateNodeDataDir(node, newDataDir); + } + ); ipcMain.handle('openDialogForNodeDataDir', (_event, nodeId: NodeId) => { return openDialogForNodeDataDir(nodeId); }); diff --git a/src/main/main.ts b/src/main/main.ts index 8638202eb..f6fe6fa8b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -88,11 +88,15 @@ const createWindow = async () => { mainWindow = new BrowserWindow({ titleBarOverlay: true, titleBarStyle: 'hiddenInset', + show: false, + minWidth: 980, + minHeight: 480, width: 1200, height: 820, icon: getAssetPath('icon.png'), webPreferences: { + enableBlinkFeatures: 'CSSColorSchemeUARendering', nodeIntegration: true, preload: app.isPackaged ? path.join(__dirname, 'preload.js') diff --git a/src/main/notifications.ts b/src/main/notifications.ts new file mode 100644 index 000000000..0b3ef5c55 --- /dev/null +++ b/src/main/notifications.ts @@ -0,0 +1,57 @@ +// import { Notification } from 'electron'; + +export type NotificationType = { + unread: boolean; + status: string; + title: string; + description: string; + timestamp: number; +}; + +export const displayNotification = () => { + // new Notification({ + // title, + // body, + // }).show(); +}; + +export const getNotifications = () => { + if (!localStorage.getItem('notifications')) { + localStorage.setItem('notifications', JSON.stringify([])); + } + return JSON.parse(localStorage.getItem('notifications') || ''); +}; + +export const removeNotifications = () => { + localStorage.setItem('notifications', JSON.stringify([])); + return []; +}; + +export const addNotification = (notification: NotificationType) => { + const notifications = getNotifications(); + notifications.push(notification); + + localStorage.setItem('notifications', JSON.stringify(notifications)); +}; + +export const addNotifications = (notifications: NotificationType[]) => { + notifications.forEach((notification: NotificationType) => { + addNotification(notification); + }); +}; + +export const markAllAsRead = () => { + const notifications = JSON.parse(localStorage.getItem('notifications') || ''); + + notifications.forEach((notification: NotificationType) => { + notification.unread = false; + }); + + localStorage.setItem('notifications', JSON.stringify(notifications)); + + return notifications; +}; + +export const initialize = async () => { + console.log('test initialize'); +}; diff --git a/src/main/preload.ts b/src/main/preload.ts index 35ccb884d..46fde57b3 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -77,6 +77,8 @@ contextBridge.exposeInMainWorld('electron', { stopNode: (nodeId: NodeId) => { ipcRenderer.invoke('stopNode', nodeId); }, + updateNodeDataDir: (node: Node, newDataDir: string) => + ipcRenderer.invoke('updateNodeDataDir', node, newDataDir), openDialogForNodeDataDir: (nodeId: NodeId) => ipcRenderer.invoke('openDialogForNodeDataDir', nodeId), openDialogForStorageLocation: () => diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 51c5ab6be..15be8439a 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,35 +1,60 @@ -import { useEffect, useState } from 'react'; -import { MemoryRouter as Router, Routes, Route } from 'react-router-dom'; +import React, { useEffect, useState } from 'react'; +import { MemoryRouter, Routes, Route, Outlet } from 'react-router-dom'; import * as Sentry from '@sentry/electron/renderer'; +import NotificationsWrapper from './Presentational/Notifications/NotificationsWrapper'; +import SystemMonitor from './Presentational/SystemMonitor/SystemMonitor'; import './Generics/redesign/globalStyle.css'; import './reset.css'; import { useAppDispatch } from './state/hooks'; import { initialize as initializeIpcListeners } from './ipc'; -import NodeScreen from './NodeScreen'; +import NodeScreen from './Presentational/NodeScreen/NodeScreen'; import DataRefresher from './DataRefresher'; import electron from './electronGlobal'; import { SidebarWrapper } from './Presentational/SidebarWrapper/SidebarWrapper'; -import NNSplash from './Presentational/NNSplashScreen/NNSplashScreen'; -import { dragWindowContainer } from './app.css'; +import LogsWrapper from './Generics/redesign/LogMessage/LogsWrapper'; +import NodeSetup from './Presentational/NodeSetup/NodeSetup'; +import { + dragWindowContainer, + homeContainer, + contentContainer, +} from './app.css'; import ThemeManager from './ThemeManager'; -import { Modal } from './Generics/redesign/Modal/Modal'; -import AddNodeStepper from './Presentational/AddNodeStepper/AddNodeStepper'; +import ModalManager from './Presentational/ModalManager/ModalManager'; Sentry.init({ dsn: electron.SENTRY_DSN, debug: true, }); -const MainScreen = () => { +const WindowContainer = ({ children }: { children: React.ReactNode }) => { + return ( + <> +
+ {children} + + ); +}; + +const Main = () => { + return ( + +
+ + + +
+
+ ); +}; + +const System = () => { + return ; +}; + +export default function App() { const dispatch = useAppDispatch(); const [sHasSeenSplashscreen, setHasSeenSplashscreen] = useState(); - const [sHasClickedGetStarted, setHasClickedGetStarted] = useState(); - const [sIsModalOpenAddNode, setIsModalOpenAddNode] = useState(); - - // const isStartOnLogin = await electron.getStoreValue('isStartOnLogin'); - // console.log('isStartOnLogin: ', isStartOnLogin); - // setIsOpenOnLogin(isStartOnLogin); useEffect(() => { const callAsync = async () => { @@ -44,81 +69,69 @@ const MainScreen = () => { initializeIpcListeners(dispatch); }, [dispatch]); - const onClickSplashGetStarted = () => { - setHasSeenSplashscreen(true); - electron.getSetHasSeenSplashscreen(true); - setHasClickedGetStarted(true); - setIsModalOpenAddNode(true); - }; - - // const onChangeOpenOnLogin = (openOnLogin: boolean) => { - // electron.setStoreValue('isStartOnLogin', openOnLogin); - // setIsOpenOnLogin(openOnLogin); - // }; if (sHasSeenSplashscreen === undefined) { console.log( 'waiting for splash screen value to return... showing loading screen' ); return <>; } + + let initialPage = '/main/node'; + // electron.getSetHasSeenSplashscreen(false); if (sHasSeenSplashscreen === false) { + initialPage = '/setup'; console.log('User has not seen the splash screen yet'); } return ( - {sHasSeenSplashscreen === false ? ( - <> - {!sHasClickedGetStarted && ( - - )} - - ) : ( - <> -
-
- -
- -
-
- - - {/* Todo: remove this when Modal Manager is created */} - setIsModalOpenAddNode(false)} - isFullScreen - > - { - console.log(newValue); - if (newValue === 'done' || newValue === 'cancel') { - setIsModalOpenAddNode(false); - } - }} + + + + + + + + } /> - - - )} + }> + + +
+ } + /> + } /> + + +
+ } + /> + + + + } + /> + + {/* Using path="*"" means "match anything", so this route + acts like a catch-all for URLs that we don't have explicit + routes for. */} + {/* } /> */} + + + ); -}; - -export default function App() { - return ( - - - } /> - - - ); } diff --git a/src/renderer/Generics/redesign/Button/Button.tsx b/src/renderer/Generics/redesign/Button/Button.tsx index 7c5b39005..b6490f5d0 100644 --- a/src/renderer/Generics/redesign/Button/Button.tsx +++ b/src/renderer/Generics/redesign/Button/Button.tsx @@ -14,10 +14,8 @@ export interface ButtonProps { /** * Is this the principal call to action on the page? */ - primary?: boolean; disabled?: boolean; backgroundColor?: string; - ghost?: boolean; spaceBetween?: boolean; size?: 'small' | 'medium' | 'large'; /** @@ -44,7 +42,6 @@ export interface ButtonProps { } const Button = ({ - primary = false, size = 'medium', disabled = false, variant = 'text', @@ -52,13 +49,11 @@ const Button = ({ iconId = 'settings', spaceBetween = false, wide = false, - ghost = false, backgroundColor, label, ...props }: ButtonProps) => { - // initialization makes type backwards compatible with primary - let buttonStyle = primary ? primaryButton : secondaryButton; + let buttonStyle; if (type === 'secondary') { buttonStyle = secondaryButton; } else if (type === 'primary') { @@ -68,9 +63,6 @@ const Button = ({ } else if (type === 'danger') { buttonStyle = dangerButton; } - if (ghost) { - buttonStyle = ghostButton; - } const classNames = [baseButton, buttonStyle]; const wideStyle = wide ? 'wide' : ''; const spaceBetweenStyle = spaceBetween ? 'spaceBetween' : ''; diff --git a/src/renderer/Generics/redesign/Button/button.css.ts b/src/renderer/Generics/redesign/Button/button.css.ts index 396281e7e..3f38b552a 100644 --- a/src/renderer/Generics/redesign/Button/button.css.ts +++ b/src/renderer/Generics/redesign/Button/button.css.ts @@ -10,7 +10,7 @@ export const baseButton = style({ justifyContent: 'center', alignItems: 'center', padding: '8px 12px', - gap: '10px', + gap: 6, border: '1px solid', borderRadius: 5, borderColor: vars.color.font10, diff --git a/src/renderer/Generics/redesign/Checklist/checklist.css.ts b/src/renderer/Generics/redesign/Checklist/checklist.css.ts index 920abc8a2..21c60a5e9 100644 --- a/src/renderer/Generics/redesign/Checklist/checklist.css.ts +++ b/src/renderer/Generics/redesign/Checklist/checklist.css.ts @@ -5,4 +5,5 @@ export const container = style({ flexDirection: 'column', alignItems: 'flex-start', gap: 16, + paddingBottom: 15, }); diff --git a/src/renderer/Generics/redesign/ContentWithSideArt/ContentWithSideArt.tsx b/src/renderer/Generics/redesign/ContentWithSideArt/ContentWithSideArt.tsx index ca931b287..36c83e424 100644 --- a/src/renderer/Generics/redesign/ContentWithSideArt/ContentWithSideArt.tsx +++ b/src/renderer/Generics/redesign/ContentWithSideArt/ContentWithSideArt.tsx @@ -1,25 +1,29 @@ import { - contentContianer, - contianer, - graphicsContianer, + contentContainer, + container, + graphicsContainer, } from './contentWithSideArt.css'; import defaultGraphic from '../../../assets/images/artwork/NN-Onboarding-Artwork-01.png'; type Props = { children: React.ReactNode; graphic?: string; + modal: boolean; }; -const ContentWithSideArt = ({ children, graphic }: Props) => { +const ContentWithSideArt = ({ children, graphic, modal }: Props) => { + const modalStyle = modal ? 'modal' : ''; return ( -
-
{children}
+
+
{children}
{/* art graphic - background image matches content height more easily */} -
+ {!modal && ( +
+ )}
); }; diff --git a/src/renderer/Generics/redesign/ContentWithSideArt/contentWithSideArt.css.ts b/src/renderer/Generics/redesign/ContentWithSideArt/contentWithSideArt.css.ts index b481f7e9d..08855864d 100644 --- a/src/renderer/Generics/redesign/ContentWithSideArt/contentWithSideArt.css.ts +++ b/src/renderer/Generics/redesign/ContentWithSideArt/contentWithSideArt.css.ts @@ -1,13 +1,13 @@ import { style } from '@vanilla-extract/css'; -export const contianer = style({ +export const container = style({ display: 'flex', flexDirection: 'row', width: '100%', height: '100%', }); -export const graphicsContianer = style({ +export const graphicsContainer = style({ minWidth: 380, // min-width works with flexGrow: 1 on the content '@media': { 'screen and (max-width: 980px)': { @@ -16,4 +16,12 @@ export const graphicsContianer = style({ }, }); -export const contentContianer = style({ flexGrow: 1, paddingRight: 32 }); +export const contentContainer = style({ + flexGrow: 1, + padding: '80px 64px', + selectors: { + [`&.modal`]: { + padding: 0, + }, + }, +}); diff --git a/src/renderer/Generics/redesign/DynamicSettings/DynamicSettings.tsx b/src/renderer/Generics/redesign/DynamicSettings/DynamicSettings.tsx index 44d02ab58..6611bf745 100644 --- a/src/renderer/Generics/redesign/DynamicSettings/DynamicSettings.tsx +++ b/src/renderer/Generics/redesign/DynamicSettings/DynamicSettings.tsx @@ -3,7 +3,7 @@ import { ConfigTranslationMap, ConfigValuesMap, } from '../../../../common/nodeConfig'; -import { SettingChangeHandler } from '../../../Presentational/NodeSettingsModal/NodeSettingsWrapper'; +import { SettingChangeHandler } from '../../../Presentational/NodeSettings/NodeSettingsWrapper'; import LineLabelSettings from '../LabelSetting/LabelSettings'; import { LabelSettingsSectionProps } from '../LabelSetting/LabelValuesSection'; import convertConfigToLabelSettings from './convertConfigToLabelSettings'; @@ -17,12 +17,14 @@ export type DynamicSettingsProps = { configValuesMap?: ConfigValuesMap; isDisabled?: boolean; onChange?: SettingChangeHandler; + type?: string; }; const DynamicSettings = ({ categoryConfigs, configValuesMap, isDisabled, onChange, + type, }: DynamicSettingsProps) => { const [sSections, setSections] = useState({ items: [], @@ -45,7 +47,7 @@ const DynamicSettings = ({ return ( <> - + ); }; diff --git a/src/renderer/Generics/redesign/DynamicSettings/Setting.tsx b/src/renderer/Generics/redesign/DynamicSettings/Setting.tsx index ebbeb1676..0a483563e 100644 --- a/src/renderer/Generics/redesign/DynamicSettings/Setting.tsx +++ b/src/renderer/Generics/redesign/DynamicSettings/Setting.tsx @@ -9,7 +9,7 @@ import FolderInput from '../Input/FolderInput'; import Input from '../Input/Input'; import Select from '../Select/Select'; import MultiSelect from '../Select/MultiSelect'; -import { SettingChangeHandler } from '../../../Presentational/NodeSettingsModal/NodeSettingsWrapper'; +import { SettingChangeHandler } from '../../../Presentational/NodeSettings/NodeSettingsWrapper'; export type SettingProps = { configTranslation: ConfigTranslation; diff --git a/src/renderer/Generics/redesign/DynamicSettings/convertConfigToLabelSettings.tsx b/src/renderer/Generics/redesign/DynamicSettings/convertConfigToLabelSettings.tsx index f9f7e1f3e..124779484 100644 --- a/src/renderer/Generics/redesign/DynamicSettings/convertConfigToLabelSettings.tsx +++ b/src/renderer/Generics/redesign/DynamicSettings/convertConfigToLabelSettings.tsx @@ -2,7 +2,7 @@ import { ConfigTranslation, ConfigValuesMap, } from '../../../../common/nodeConfig'; -import { SettingChangeHandler } from '../../../Presentational/NodeSettingsModal/NodeSettingsWrapper'; +import { SettingChangeHandler } from '../../../Presentational/NodeSettings/NodeSettingsWrapper'; import { LabelSettingsItem, LabelSettingsSectionProps, diff --git a/src/renderer/Generics/redesign/Header/Header.tsx b/src/renderer/Generics/redesign/Header/Header.tsx index e00124578..151df86d8 100644 --- a/src/renderer/Generics/redesign/Header/Header.tsx +++ b/src/renderer/Generics/redesign/Header/Header.tsx @@ -1,4 +1,7 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { setModalState } from '../../../state/modal'; +import { useAppDispatch } from '../../../state/hooks'; import Button, { ButtonProps } from '../Button/Button'; import { NodeOverviewProps } from '../consts'; import { NodeIcon } from '../NodeIcon/NodeIcon'; @@ -18,8 +21,6 @@ import { popupContainer, menuButtonContainer, } from './header.css'; -import NodeSettingsWrapper from '../../../Presentational/NodeSettingsModal/NodeSettingsWrapper'; -import LogsModal from '../../../Presentational/NodeLogsModal/LogsModal'; /** * Primary UI component for user interaction @@ -30,9 +31,9 @@ export const Header = (props: NodeOverviewProps) => { const [isCalloutDisplayed, setIsCalloutDisplayed] = useState(false); const [isSettingsDisplayed, setIsSettingsDisplayed] = useState(false); - const [sIsSettingsModalOpen, setIsSettingsModalOpen] = - useState(false); - const [sIsLogsModalOpen, setIsLogsModalOpen] = useState(false); + + const navigate = useNavigate(); + const dispatch = useAppDispatch(); const startStopButtonProps: ButtonProps = { label: '', @@ -87,7 +88,7 @@ export const Header = (props: NodeOverviewProps) => { >
- - setIsSettingsModalOpen(false)} - /> - setIsLogsModalOpen(false)} - />
); }; diff --git a/src/renderer/Generics/redesign/HeaderButton/headerButton.css.ts b/src/renderer/Generics/redesign/HeaderButton/headerButton.css.ts index 71c9215d2..d50da9c39 100644 --- a/src/renderer/Generics/redesign/HeaderButton/headerButton.css.ts +++ b/src/renderer/Generics/redesign/HeaderButton/headerButton.css.ts @@ -13,6 +13,7 @@ export const container = style({ width: 28, height: 28, borderRadius: 5, + zIndex: 4, color: vars.color.font, ':hover': { backgroundColor: vars.components.headerButtonHover, diff --git a/src/renderer/Generics/redesign/Input/folderInput.css.ts b/src/renderer/Generics/redesign/Input/folderInput.css.ts index ad40b0d6a..cef6c6400 100644 --- a/src/renderer/Generics/redesign/Input/folderInput.css.ts +++ b/src/renderer/Generics/redesign/Input/folderInput.css.ts @@ -7,6 +7,7 @@ export const container = style({ gap: 8, width: '100%', minWidth: 400, + height: 52, }); export const pathAndChangeContainer = style({ diff --git a/src/renderer/Generics/redesign/LabelSetting/LabelSettings.tsx b/src/renderer/Generics/redesign/LabelSetting/LabelSettings.tsx index 5bd7606f2..93071b71b 100644 --- a/src/renderer/Generics/redesign/LabelSetting/LabelSettings.tsx +++ b/src/renderer/Generics/redesign/LabelSetting/LabelSettings.tsx @@ -20,6 +20,7 @@ export interface LineLabelSettingsProps { * Column mode? */ column?: boolean; + type?: string; } /** @@ -29,6 +30,7 @@ const LineLabelSettings = ({ title, items, column, + type, }: LineLabelSettingsProps) => { let columnDiv = ''; let columnContainer = ''; @@ -45,7 +47,7 @@ const LineLabelSettings = ({ // Settings section ordering does not change during view of modal // eslint-disable-next-line react/no-array-index-key
- +
))}
diff --git a/src/renderer/Generics/redesign/LabelSetting/LabelValuesSection.tsx b/src/renderer/Generics/redesign/LabelSetting/LabelValuesSection.tsx index db29820df..030c80097 100644 --- a/src/renderer/Generics/redesign/LabelSetting/LabelValuesSection.tsx +++ b/src/renderer/Generics/redesign/LabelSetting/LabelValuesSection.tsx @@ -27,16 +27,18 @@ export interface LabelSettingsSectionProps { * The sections label value items */ items: LabelSettingsItem[]; + type?: string; } const LabelSettingsSection = ({ sectionTitle, items, + type, }: LabelSettingsSectionProps) => { const { t } = useTranslation('genericComponents'); return ( -
+
{sectionTitle && (
{sectionTitle}
diff --git a/src/renderer/Generics/redesign/LabelSetting/labelSettingsSection.css.ts b/src/renderer/Generics/redesign/LabelSetting/labelSettingsSection.css.ts index c639fdbfe..a30d67cec 100644 --- a/src/renderer/Generics/redesign/LabelSetting/labelSettingsSection.css.ts +++ b/src/renderer/Generics/redesign/LabelSetting/labelSettingsSection.css.ts @@ -6,6 +6,11 @@ export const sectionContainer = style({ flexDirection: 'column', alignItems: 'flex-start', padding: '8px 0px', + selectors: { + [`&.modal`]: { + padding: '0px 0px 8px 0px', + }, + }, }); export const sectionHeaderContainer = style({ display: 'flex', diff --git a/src/renderer/Generics/redesign/LogMessage/Logs.tsx b/src/renderer/Generics/redesign/LogMessage/Logs.tsx index 8ba63f4a4..65590edaf 100644 --- a/src/renderer/Generics/redesign/LogMessage/Logs.tsx +++ b/src/renderer/Generics/redesign/LogMessage/Logs.tsx @@ -1,5 +1,6 @@ import React, { SetStateAction, useState, useEffect } from 'react'; import moment from 'moment'; +import { useNavigate } from 'react-router-dom'; import { container, filterContainer, @@ -24,7 +25,6 @@ export interface LogsProps { * sLogs props */ sLogs: LogWithMetadata[]; - onClickCloseButton: () => void; } const timeframes = { @@ -71,7 +71,7 @@ const isWithinTimeframe = (timestamp: number, timeframe: number) => { return moment(timestamp).isBetween(beforeTime, nowTime); }; -export const Logs = ({ sLogs, onClickCloseButton }: LogsProps) => { +export const Logs = ({ sLogs }: LogsProps) => { const [logs, setLogs] = useState([]); const [isFilterBarDisplayed, setIsFilterBarDisplayed] = useState(false); @@ -133,6 +133,8 @@ export const Logs = ({ sLogs, onClickCloseButton }: LogsProps) => { const typeLabel = typeLabels[typeFilter]; const timeframeLabel = timeframeLabels[timeframeFilter]; + const navigate = useNavigate(); + return ( <>
@@ -142,7 +144,7 @@ export const Logs = ({ sLogs, onClickCloseButton }: LogsProps) => { title="Logs" leftButtonIconId="down" rightButtonIconId="filter" - leftButtonOnClick={onClickCloseButton} + leftButtonOnClick={() => navigate('/main/node')} rightButtonOnClick={() => { if (isFilterBarDisplayed) { setIsFilterBarDisplayed(false); diff --git a/src/renderer/Generics/redesign/LogMessage/LogsWrapper.tsx b/src/renderer/Generics/redesign/LogMessage/LogsWrapper.tsx new file mode 100644 index 000000000..fa3b6c292 --- /dev/null +++ b/src/renderer/Generics/redesign/LogMessage/LogsWrapper.tsx @@ -0,0 +1,63 @@ +import { useCallback, useEffect, useState } from 'react'; +import electron from '../../../electronGlobal'; +import { LogWithMetadata } from '../../../../main/util/nodeLogUtils'; +import { useAppSelector } from '../../../state/hooks'; +import { selectSelectedNodeId } from '../../../state/node'; + +import { Logs } from './Logs'; + +export type ThemeSetting = 'light' | 'dark' | 'auto'; +export type Preference = 'theme' | 'isOpenOnStartup'; + +const LogsWrapper = () => { + const sSelectedNodeId = useAppSelector(selectSelectedNodeId); + const [sLogs, setLogs] = useState([]); + + const nodeLogsListener = (message: LogWithMetadata[]) => { + setLogs((prevState) => { + if (prevState.length < 1000) { + return [...prevState, message[0]]; + } + return [message[0]]; + }); + }; + + const listenForNodeLogs = useCallback(async () => { + electron.ipcRenderer.on('nodeLogs', nodeLogsListener); + }, []); + + useEffect(() => { + console.log('LogsWrapper: isOpen. Listening for logs.'); + listenForNodeLogs(); + return () => { + setLogs([]); + electron.ipcRenderer.removeAllListeners('nodeLogs'); + }; + }, [listenForNodeLogs]); + + useEffect(() => { + // when switching selected nodes... + // if none selected, send stop + // if one is selected, ask for those logs + setLogs([]); + console.log('LogsWrapper: isOpen, sSelectedNodeId changed. Clear logs.'); + if (sSelectedNodeId) { + electron.sendNodeLogs(sSelectedNodeId); + console.log( + 'LogsWrapper: isOpen && sSelectedNodeId truthy. Send selected node logs' + ); + } else { + console.log( + 'LogsWrapper: isOpen && sSelectedNodeId falsy. stopSendingNodeLogs' + ); + electron.stopSendingNodeLogs(); + } + return () => { + electron.stopSendingNodeLogs(); + }; + }, [sSelectedNodeId]); + + return ; +}; + +export default LogsWrapper; diff --git a/src/renderer/Generics/redesign/LogMessage/logMessage.css.ts b/src/renderer/Generics/redesign/LogMessage/logMessage.css.ts index ce6ee3e0c..b4546f7ae 100644 --- a/src/renderer/Generics/redesign/LogMessage/logMessage.css.ts +++ b/src/renderer/Generics/redesign/LogMessage/logMessage.css.ts @@ -8,7 +8,7 @@ export const container = style({ alignItems: 'flex-start', padding: '4px 0px', gap: 8, - borderBottom: '1px solid rgba(0, 0, 2, 0.04)', + borderBottom: `1px solid ${vars.color.background96}`, flex: 'none', order: 2, alignSelf: 'stretch', diff --git a/src/renderer/Generics/redesign/LogMessage/logs.css.ts b/src/renderer/Generics/redesign/LogMessage/logs.css.ts index 1f466db6c..e0d702d49 100644 --- a/src/renderer/Generics/redesign/LogMessage/logs.css.ts +++ b/src/renderer/Generics/redesign/LogMessage/logs.css.ts @@ -8,6 +8,7 @@ export const container = style({ height: '100%', display: 'flex', flexDirection: 'column', + flex: 1, }); export const logsContainer = style({ diff --git a/src/renderer/Generics/redesign/Modal/Modal.tsx b/src/renderer/Generics/redesign/Modal/Modal.tsx index d968771fc..22ff8a08d 100644 --- a/src/renderer/Generics/redesign/Modal/Modal.tsx +++ b/src/renderer/Generics/redesign/Modal/Modal.tsx @@ -1,35 +1,56 @@ import { useCallback, useEffect } from 'react'; -import Button from '../Button/Button'; +import Button, { ButtonProps } from '../Button/Button'; import { + modalHeaderContainer, modalBackdropStyle, + modalCloseButton, modalChildrenContainer, modalContentStyle, + modalStepperContainer, titleFont, } from './modal.css'; +import { ModalConfig } from '../../../Presentational/ModalManager/modalUtils'; type Props = { + modalType?: 'alert' | 'modal'; + modalStyle?: string; + modalTitle: string; + backButtonEnabled?: boolean; children: React.ReactElement[] | React.ReactElement; - isOpen: boolean | undefined; - onClickCloseButton: () => void; - title?: string; - isFullScreen?: boolean; + buttonCancelLabel?: string; + buttonSaveLabel?: string; + buttonSaveType?: ButtonProps['type']; + buttonSaveVariant?: ButtonProps['variant']; + buttonSaveIcon?: ButtonProps['iconId']; + isSaveButtonDisabled?: boolean; + modalOnSaveConfig: (updatedConfig: ModalConfig | undefined) => void; + modalOnClose: () => void; + modalOnCancel: () => void; }; export const Modal = ({ children, - isOpen, - onClickCloseButton, - title, - isFullScreen, + modalType = 'modal', + modalStyle = '', + modalTitle = '', + backButtonEnabled = true, + buttonCancelLabel = 'Cancel', + buttonSaveLabel = 'Save', + buttonSaveType = 'primary', + buttonSaveVariant = 'text', + buttonSaveIcon = 'play', + isSaveButtonDisabled = false, + modalOnSaveConfig, + modalOnClose, + modalOnCancel, }: Props) => { const escFunction = useCallback( (event: { key: string }) => { if (event.key === 'Escape') { - // Do whatever when esc is pressed - onClickCloseButton(); + modalOnClose(); } }, - [onClickCloseButton] + [modalOnClose] ); useEffect(() => { @@ -41,37 +62,46 @@ export const Modal = ({ }, [escFunction]); return ( -
-
-
-
-
-
+
+
+ {modalType !== 'alert' && ( +
+
- - {title} - + )} +
+ {modalTitle} +
+
+ {children} +
+
+ {backButtonEnabled && ( +
-
{children}
); diff --git a/src/renderer/Generics/redesign/Modal/modal.css.ts b/src/renderer/Generics/redesign/Modal/modal.css.ts index 68eb6e125..51755b2e0 100644 --- a/src/renderer/Generics/redesign/Modal/modal.css.ts +++ b/src/renderer/Generics/redesign/Modal/modal.css.ts @@ -3,15 +3,15 @@ import { vars } from '../theme.css'; export const modalBackdropStyle = style({ boxSizing: 'border-box', - display: 'none', + display: 'flex', position: 'fixed', - zIndex: 1, + zIndex: 2, alignItems: 'center', justifyContent: 'center', left: '0', top: '0', - paddingTop: 30, - paddingBottom: 30, + paddingTop: 48, + paddingBottom: 48, width: '100%', height: '100%', overflow: 'auto', @@ -25,16 +25,38 @@ export const modalContentStyle = style({ boxSizing: 'border-box', filter: 'drop-shadow(0px 32px 64px rgba(0, 0, 0, 0.1876)) drop-shadow(0px 2px 21px rgba(0, 0, 0, 0.1474))', - maxHeight: '90vh', + maxHeight: '80vh', backgroundColor: vars.color.background, - padding: '32px', paddingTop: '0px', borderRadius: 14, - width: '80%', top: '50%', left: '50%', color: 'inherit', + justifyContent: 'space-between', // a: { color: 'inherit' }, + selectors: { + [`&.addNode`]: { + // maxHeight: 'none', + }, + [`&.nodeSettings`]: { + // maxHeight: 'none', + }, + }, +}); + +export const modalCloseButton = style({ + position: 'absolute', + top: 12, + right: 14, +}); + +export const modalHeaderContainer = style({ + padding: '40px 32px 16px 32px', + selectors: { + [`&.alert`]: { + padding: '24px 24px 6px 24px', + }, + }, }); export const titleFont = style({ @@ -42,11 +64,49 @@ export const titleFont = style({ lineHeight: '28px', fontWeight: 590, paddingBottom: 16, + flexGrow: 1, + selectors: { + [`&.alert`]: { + fontSize: '15px', + lineHeight: '20px', + fontWeight: 590, + paddingBottom: 6, + }, + }, +}); + +export const modalStepperContainer = style({ + borderTop: `1px solid ${vars.color.background92}`, + display: 'flex', + flexDirection: 'row', + padding: 14, + justifyContent: 'flex-end', + gap: 8, + selectors: { + [`&.alert`]: { + borderTop: 'none', + padding: 24, + }, + }, }); export const modalChildrenContainer = style({ + padding: '0px 32px', flex: 1, overflow: 'auto', - // extra padding from scrollbar - paddingRight: 10, + selectors: { + [`&.alert`]: { + padding: '0px 24px', + overflow: 'hidden', + }, + [`&.addNode`]: { + overflowY: 'auto', + overflowX: 'hidden', + }, + [`&.nodeSettings`]: { + padding: 0, + overflowX: 'hidden', + // minHeight: 559, + }, + }, }); diff --git a/src/renderer/Generics/redesign/NotificationIcon/NotificationIcon.tsx b/src/renderer/Generics/redesign/NotificationIcon/NotificationIcon.tsx new file mode 100644 index 000000000..1f5271472 --- /dev/null +++ b/src/renderer/Generics/redesign/NotificationIcon/NotificationIcon.tsx @@ -0,0 +1,82 @@ +import { IconId } from 'renderer/assets/images/icons'; +import { + iconBackground, + hasStatusStyle, + smallStyle, + statusStyle, + containerStyle, +} from './notificationIcon.css'; +import { Icon } from '../Icon/Icon'; +import { common } from '../theme.css'; + +export interface NotificationIconProps { + /** + * What's the status? + */ + status: 'info' | 'completed' | 'download' | 'warning' | 'error'; + /** + * Is it unread? + */ + unread?: boolean; +} + +/** + * Primary UI component for user interaction + */ +export const NotificationIcon = ({ status, unread }: NotificationIconProps) => { + let statusComponent = null; + let unreadStyle = ''; + if (unread) { + statusComponent =
; + unreadStyle = hasStatusStyle; + } + + const iconObject = { + color: '', + backgroundColor: '', + iconId: 'infocirclefilled', + }; + switch (status) { + case 'info': + iconObject.color = common.color.green500; + iconObject.backgroundColor = 'rgba(18, 186, 108, 0.08)'; + iconObject.iconId = 'infocirclefilled'; + break; + case 'completed': + iconObject.color = common.color.green500; + iconObject.backgroundColor = 'rgba(18, 186, 108, 0.08)'; + iconObject.iconId = 'checkcirclefilled'; + break; + case 'download': + iconObject.color = common.color.blue500; + iconObject.backgroundColor = 'rgba(19, 122, 248, 0.08)'; + iconObject.iconId = 'download1'; + break; + case 'warning': + iconObject.color = common.color.orange400; + iconObject.backgroundColor = 'rgba(247, 144, 9, 0.12)'; + iconObject.iconId = 'warningcirclefilled'; + break; + default: + } + + return ( +
+ {/* https://stackoverflow.com/questions/6040005/relatively-position-an-element-without-it-taking-up-space-in-document-flow */} +
+ {statusComponent} +
+
+
+ +
+
+
+ ); +}; diff --git a/src/renderer/Generics/redesign/NotificationIcon/notificationIcon.css.ts b/src/renderer/Generics/redesign/NotificationIcon/notificationIcon.css.ts new file mode 100644 index 000000000..38da08de7 --- /dev/null +++ b/src/renderer/Generics/redesign/NotificationIcon/notificationIcon.css.ts @@ -0,0 +1,59 @@ +import { style } from '@vanilla-extract/css'; +import { vars, common } from '../theme.css'; + +export const imageStyle = style({ + // position: 'relative', + width: '100%', + height: '100%', + objectFit: 'contain', +}); + +export const hasStatusStyle = style({ + WebkitMaskImage: + 'radial-gradient(circle 8px at calc(100% - 23px) calc(100% - 23px),transparent 6px,#000 0)', +}); +export const smallStyle = style({}); + +export const iconBackground = style({ + // position: 'relative', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + boxSizing: 'border-box', + MozBoxSizing: 'border-box', + WebkitBoxSizing: 'border-box', + width: 24, + height: 24, + borderRadius: 5, + boxShadow: '0px 1px 2px rgba(0, 0, 0, 0.16)', +}); + +export const containerStyle = style({ + width: 24, + height: 24, +}); + +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', + zIndex: 1, + color: vars.color.font70, + background: common.color.red500, + top: '-3px', + left: '-3px', + width: 8, + height: 8, + borderRadius: 4, +}); diff --git a/src/renderer/Generics/redesign/NotificationItem/NotificationItem.tsx b/src/renderer/Generics/redesign/NotificationItem/NotificationItem.tsx new file mode 100644 index 000000000..60fba5060 --- /dev/null +++ b/src/renderer/Generics/redesign/NotificationItem/NotificationItem.tsx @@ -0,0 +1,84 @@ +import moment from 'moment'; +import { NotificationIcon } from '../NotificationIcon/NotificationIcon'; +import { + container, + iconContainer, + textContainer, + titleStyle, + infoStyle, + rowContainer, + dateStyle, +} from './notificationItem.css'; + +export type NotificationStatus = + | 'info' + | 'completed' + | 'download' + | 'warning' + | 'error'; +export interface NotificationItemProps { + /** + * Unread status + */ + unread: boolean; + /** + * Notification title + */ + title: string; + /** + * Notification description + */ + description?: string; + /** + * What's the status? + */ + status: NotificationStatus; + /** + * Timestamp of when the event occurred + */ + timestamp?: number; + /** + * Optional click handler + */ + onClick?: () => void; +} + +export const NotificationItem = ({ + onClick, + title, + description, + status, + unread, + timestamp, +}: NotificationItemProps) => { + const onClickAction = () => { + if (onClick) { + onClick(); + } + }; + + const containerStyles = [container]; + + return ( +
+
+ +
+
+
+
{title}
+
+ {moment(timestamp).format('MMM Do h:mm a')} +
+
+
{description}
+
+
+ ); +}; diff --git a/src/renderer/Generics/redesign/NotificationItem/notificationItem.css.ts b/src/renderer/Generics/redesign/NotificationItem/notificationItem.css.ts new file mode 100644 index 000000000..d896064c7 --- /dev/null +++ b/src/renderer/Generics/redesign/NotificationItem/notificationItem.css.ts @@ -0,0 +1,77 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '../theme.css'; + +export const container = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + padding: '16px 0px 16px 10px', + gap: '12px', + width: 'auto', + cursor: 'pointer', + userSelect: 'none', + borderBottom: vars.components.clientCardBorder, +}); + +export const selectedContainer = style({ + background: vars.color.background96, + borderRadius: '4px', +}); + +export const iconContainer = style({ + flex: 'none', + order: '0', + flexGrow: '0', +}); + +export const textContainer = style({ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + padding: '0px', + gap: '2px', + // width: '172px', + height: '32px', + flex: 'none', + order: '1', + flexGrow: '1', +}); + +export const rowContainer = style({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + width: '100%', + justifyContent: 'space-between', +}); + +export const dateStyle = style({ + fontWeight: '400', + fontSize: '11px', + lineHeight: '14px', + color: vars.color.font50, +}); + +export const titleStyle = style({ + order: '0', + flex: 'none', + alignSelf: 'stretch', + flexGrow: '0', + fontWeight: '510', + fontSize: '13px', + lineHeight: '16px', + letterSpacing: '-0.12px', + color: vars.color.font85, +}); + +export const infoStyle = style({ + order: 1, + flex: 'none', + alignSelf: 'stretch', + flexGrow: 0, + fontWeight: '400', + fontSize: '13px', + lineHeight: '16px', + letterSpacing: '-0.08px', + color: vars.color.font70, +}); diff --git a/src/renderer/Generics/redesign/SidebarLinkItem/SidebarLinkItem.tsx b/src/renderer/Generics/redesign/SidebarLinkItem/SidebarLinkItem.tsx index 8fa6a77e8..4032875b6 100644 --- a/src/renderer/Generics/redesign/SidebarLinkItem/SidebarLinkItem.tsx +++ b/src/renderer/Generics/redesign/SidebarLinkItem/SidebarLinkItem.tsx @@ -58,7 +58,7 @@ export const SidebarLinkItem = ({ >
{label}
- {count && } + {count !== undefined && count > 0 && }
); }; diff --git a/src/renderer/Generics/redesign/SpecialSelect/SpecialSelect.tsx b/src/renderer/Generics/redesign/SpecialSelect/SpecialSelect.tsx index df6f659ff..58628f529 100644 --- a/src/renderer/Generics/redesign/SpecialSelect/SpecialSelect.tsx +++ b/src/renderer/Generics/redesign/SpecialSelect/SpecialSelect.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/destructuring-assignment */ // Options replaceable component docs: // https://react-select.com/components#Option -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import Select, { OptionProps, ValueContainerProps } from 'react-select'; import SelectCard, { SelectCardProps } from '../SelectCard/SelectCard'; import { vars } from '../theme.css'; @@ -34,39 +34,24 @@ export type SelectOption = { export interface SpecialSelectProps { options?: SelectOption[]; onChange?: (newValue: SelectOption | undefined) => void; + selectedOption: SelectOption; } /** * Used for selecting Ethereum node client */ -const SpecialSelect = ({ options, onChange }: 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 SpecialSelect = ({ + options, + onChange, + selectedOption, +}: SpecialSelectProps) => { + const [sSelectedOption, setSelectedOption] = + useState(selectedOption); const onSelectChange = (newValue: unknown) => { const newlySelectedOption = newValue as SelectOption; console.log('onSelectChange: ', newlySelectedOption); + if (onChange) onChange(newlySelectedOption); setSelectedOption(newlySelectedOption); }; diff --git a/src/renderer/Generics/redesign/Splash/Splash.tsx b/src/renderer/Generics/redesign/Splash/Splash.tsx index 90e1871e0..591f6dd54 100644 --- a/src/renderer/Generics/redesign/Splash/Splash.tsx +++ b/src/renderer/Generics/redesign/Splash/Splash.tsx @@ -45,7 +45,7 @@ const Splash = ({
{description}
diff --git a/src/renderer/Generics/redesign/Stepper/Stepper.tsx b/src/renderer/Generics/redesign/Stepper/Stepper.tsx index c8958be49..3ed12e34b 100644 --- a/src/renderer/Generics/redesign/Stepper/Stepper.tsx +++ b/src/renderer/Generics/redesign/Stepper/Stepper.tsx @@ -3,24 +3,27 @@ import Button from '../Button/Button'; import { bottomBar, previousButton, nextButton } from './stepper.css'; export interface StepperProps { + step?: number; /** * When a step changes ('previous' or 'next') */ onChange: (change: 'next' | 'previous') => void; } -const Stepper = ({ onChange }: StepperProps) => { +const Stepper = ({ onChange, step }: StepperProps) => { const { t } = useTranslation('genericComponents'); return (
-
diff --git a/src/renderer/Generics/redesign/SystemMonitor/SystemMonitor.tsx b/src/renderer/Generics/redesign/SystemMonitor/SystemMonitor.tsx index f287b26dd..816e48cc1 100644 --- a/src/renderer/Generics/redesign/SystemMonitor/SystemMonitor.tsx +++ b/src/renderer/Generics/redesign/SystemMonitor/SystemMonitor.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import si from 'systeminformation'; import electron from '../../../electronGlobal'; -import { container } from './systemMonitor.css'; import { LabelValuesSectionProps } from '../LabelValues/LabelValuesSection'; import LabelValues from '../LabelValues/LabelValues'; @@ -119,9 +118,8 @@ export const SystemMonitor = () => { getData(); }, []); - return ( -
- -
- ); + if (!sData) { + return
Fetching...
; // Get design spec for this case + } + return ; }; diff --git a/src/renderer/Generics/redesign/SystemMonitor/systemMonitor.css.ts b/src/renderer/Generics/redesign/SystemMonitor/systemMonitor.css.ts deleted file mode 100644 index 7cdb634b2..000000000 --- a/src/renderer/Generics/redesign/SystemMonitor/systemMonitor.css.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { style } from '@vanilla-extract/css'; - -export const container = style({ - padding: 8, -}); diff --git a/src/renderer/Generics/redesign/TabItem/tabItem.css.ts b/src/renderer/Generics/redesign/TabItem/tabItem.css.ts index e228e9cf8..7582951ea 100644 --- a/src/renderer/Generics/redesign/TabItem/tabItem.css.ts +++ b/src/renderer/Generics/redesign/TabItem/tabItem.css.ts @@ -5,6 +5,10 @@ export const container = style({ listStyleType: 'none', fontWeight: 590, fontSize: '13px', + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', lineHeight: '16px', letterSpacing: '-0.12px', color: vars.color.font50, diff --git a/src/renderer/Generics/redesign/Tabs/Tabs.tsx b/src/renderer/Generics/redesign/Tabs/Tabs.tsx index 95dad409e..2a09caf8f 100644 --- a/src/renderer/Generics/redesign/Tabs/Tabs.tsx +++ b/src/renderer/Generics/redesign/Tabs/Tabs.tsx @@ -40,7 +40,7 @@ export const Tabs = ({ children, id, modal }: TabsProps) => { return (
-
    +
      {children.map((child) => { const childId = child.props.id; diff --git a/src/renderer/Generics/redesign/Tabs/tabs.css.ts b/src/renderer/Generics/redesign/Tabs/tabs.css.ts index 9a3e1fa66..81b2173d0 100644 --- a/src/renderer/Generics/redesign/Tabs/tabs.css.ts +++ b/src/renderer/Generics/redesign/Tabs/tabs.css.ts @@ -22,15 +22,23 @@ export const tabsList = style({ height: 28, alignItems: 'center', padding: '10px 0', + selectors: { + '&.modal': { + paddingLeft: 32, + }, + }, }); export const tabContent = style({ paddingTop: 36, selectors: { '&.modal': { - overflow: 'auto', + boxSizing: 'border-box', + overflowY: 'hidden', // extra padding from scrollbar - paddingRight: 10, + padding: '36px 32px 0px 32px', + height: 'auto', + width: 624, }, }, }); diff --git a/src/renderer/Generics/redesign/UpdateCallout/UpdateCallout.tsx b/src/renderer/Generics/redesign/UpdateCallout/UpdateCallout.tsx index e0a4b92e8..6555cd514 100644 --- a/src/renderer/Generics/redesign/UpdateCallout/UpdateCallout.tsx +++ b/src/renderer/Generics/redesign/UpdateCallout/UpdateCallout.tsx @@ -32,7 +32,7 @@ export const UpdateCallout = ({ onClick }: UpdateCalloutProps) => {
diff --git a/src/renderer/Generics/redesign/theme.css.ts b/src/renderer/Generics/redesign/theme.css.ts index 0a6128e32..500998072 100644 --- a/src/renderer/Generics/redesign/theme.css.ts +++ b/src/renderer/Generics/redesign/theme.css.ts @@ -130,6 +130,7 @@ export const [lightTheme, vars] = createTheme({ font40: common.color.black40, font50: common.color.black50, font70: common.color.black70, + font85: common.color.black85, font90: common.color.black90, fontDisabled: common.color.black25, background: common.color.white100, @@ -228,6 +229,7 @@ export const darkTheme = createTheme(vars, { font40: common.color.white40, font50: common.color.white50, font70: common.color.white70, + font85: common.color.white85, font90: common.color.white90, fontDisabled: common.color.white25, background: 'rgba(28, 28, 30, 1)', diff --git a/src/renderer/Presentational/AddEthereumNode/AddEthereumNode.tsx b/src/renderer/Presentational/AddEthereumNode/AddEthereumNode.tsx index 8993d54a5..0435868a8 100644 --- a/src/renderer/Presentational/AddEthereumNode/AddEthereumNode.tsx +++ b/src/renderer/Presentational/AddEthereumNode/AddEthereumNode.tsx @@ -2,11 +2,14 @@ import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { NodeLibrary } from 'main/state/nodeLibrary'; +import { ModalConfig } from '../ModalManager/modalUtils'; import { container, descriptionFont, sectionFont, titleFont, + descriptionContainer, } from './addEthereumNode.css'; import ExternalLink from '../../Generics/redesign/Link/ExternalLink'; import SpecialSelect, { @@ -18,8 +21,6 @@ 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'; -import ContentWithSideArt from '../../Generics/redesign/ContentWithSideArt/ContentWithSideArt'; -import graphicsPng from '../../assets/images/artwork/NN-Onboarding-Artwork-01.png'; const ecOptions = [ { @@ -97,8 +98,8 @@ const ccOptions = [ ]; export type AddEthereumNodeValues = { - executionClient?: string; - consensusClient?: string; + executionClient?: SelectOption; + consensusClient?: SelectOption; storageLocation?: string; }; export interface AddEthereumNodeProps { @@ -107,10 +108,24 @@ export interface AddEthereumNodeProps { /** * Listen to node config changes */ - onChange: (newValue: AddEthereumNodeValues) => void; + onChange?: (newValue: AddEthereumNodeValues) => void; + ethereumNodeConfig?: AddEthereumNodeValues; + setConsensusClient?: ( + elClient: SelectOption, + object: AddEthereumNodeValues + ) => void; + setExecutionClient?: ( + clClient: SelectOption, + object: AddEthereumNodeValues + ) => void; + modalOnChangeConfig?: (config: ModalConfig) => void; } const AddEthereumNode = ({ + ethereumNodeConfig, + setConsensusClient, + setExecutionClient, + modalOnChangeConfig, onChange, }: /** * Todo: Pass options from the node spec files @@ -122,10 +137,12 @@ AddEthereumNodeProps) => { const { t: tGeneric } = useTranslation('genericComponents'); const [sIsOptionsOpen, setIsOptionsOpen] = useState(); const [sSelectedExecutionClient, setSelectedExecutionClient] = - useState(); + useState(ethereumNodeConfig?.executionClient || ecOptions[0]); const [sSelectedConsensusClient, setSelectedConsensusClient] = - useState(); - const [sNodeStorageLocation, setNodeStorageLocation] = useState(); + useState(ethereumNodeConfig?.consensusClient || ccOptions[0]); + const [sNodeStorageLocation, setNodeStorageLocation] = useState( + ethereumNodeConfig?.storageLocation || '' + ); const [ sNodeStorageLocationFreeStorageGBs, setNodeStorageLocationFreeStorageGBs, @@ -135,23 +152,64 @@ AddEthereumNodeProps) => { const fetchData = async () => { const defaultNodesStorageDetails = await electron.getNodesDefaultStorageLocation(); + const nodeLibrary: NodeLibrary = await electron.getNodeLibrary(); console.log('defaultNodesStorageDetails', defaultNodesStorageDetails); setNodeStorageLocation(defaultNodesStorageDetails.folderPath); + if (modalOnChangeConfig) { + modalOnChangeConfig({ + storageLocation: defaultNodesStorageDetails.folderPath, + nodeLibrary, + }); + } setNodeStorageLocationFreeStorageGBs( defaultNodesStorageDetails.freeStorageGBs ); }; fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const onChangeEc = useCallback((newEc?: SelectOption) => { - console.log('new selected execution client: ', newEc); - if (newEc) setSelectedExecutionClient(newEc.value); - }, []); - const onChangeCc = useCallback((newCc?: SelectOption) => { - console.log('new selected consensus client: ', newCc); - if (newCc) setSelectedConsensusClient(newCc.value); - }, []); + const onChangeEc = useCallback( + (newEc?: SelectOption) => { + console.log('new selected execution client: ', newEc); + const ethNodeConfig = { + executionClient: sSelectedExecutionClient, + consensusClient: sSelectedConsensusClient, + storageLocation: sNodeStorageLocation, + }; + if (newEc) { + setSelectedExecutionClient(newEc); + if (setExecutionClient) setExecutionClient(newEc, ethNodeConfig); + } + }, + [ + sSelectedExecutionClient, + sSelectedConsensusClient, + sNodeStorageLocation, + setExecutionClient, + ] + ); + + const onChangeCc = useCallback( + (newCc?: SelectOption) => { + console.log('new selected consensus client: ', newCc); + const ethNodeConfig = { + executionClient: sSelectedExecutionClient, + consensusClient: sSelectedConsensusClient, + storageLocation: sNodeStorageLocation, + }; + if (newCc) { + setSelectedConsensusClient(newCc); + if (setConsensusClient) setConsensusClient(newCc, ethNodeConfig); + } + }, + [ + sSelectedExecutionClient, + sSelectedConsensusClient, + sNodeStorageLocation, + setConsensusClient, + ] + ); useEffect(() => { if (onChange) { @@ -169,9 +227,11 @@ AddEthereumNodeProps) => { ]); return ( - -
+
+ {!modalOnChangeConfig && (
{t('LaunchAnEthereumNode')}
+ )} +
<>{t('AddEthereumNodeDescription')}
@@ -179,68 +239,81 @@ AddEthereumNodeProps) => { text={t('LearnMoreClientDiversity')} url="https://ethereum.org/en/developers/docs/nodes-and-clients/client-diversity/" /> -

Execution client

- -

Consensus client

- - setIsOptionsOpen(!sIsOptionsOpen)} - isDown={!sIsOptionsOpen} - /> - {sIsOptionsOpen && ( +
+

Recommended execution client

+ +

Recommended consensus client

+ + setIsOptionsOpen(!sIsOptionsOpen)} + isDown={!sIsOptionsOpen} + /> + {sIsOptionsOpen && ( +
+ {tGeneric('Network')}{' '}
- {tGeneric('Network')}{' '} -
-
- )} - -

{tGeneric('DataLocation')}

- { - 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 +
+ )} + +

{tGeneric('DataLocation')}

+ { + const storageLocationDetails = + await electron.openDialogForStorageLocation(); + console.log('storageLocationDetails', storageLocationDetails); + if (storageLocationDetails) { + setNodeStorageLocation(storageLocationDetails.folderPath); + if (modalOnChangeConfig) { + modalOnChangeConfig({ + storageLocation: storageLocationDetails.folderPath, + }); } - }} - /> -
- + setNodeStorageLocationFreeStorageGBs( + storageLocationDetails.freeStorageGBs + ); + } else { + // user didn't change the folder path + } + }} + /> +
); }; diff --git a/src/renderer/Presentational/AddEthereumNode/addEthereumNode.css.ts b/src/renderer/Presentational/AddEthereumNode/addEthereumNode.css.ts index 79c45656b..5bcaa255c 100644 --- a/src/renderer/Presentational/AddEthereumNode/addEthereumNode.css.ts +++ b/src/renderer/Presentational/AddEthereumNode/addEthereumNode.css.ts @@ -6,6 +6,7 @@ export const container = style({ flexDirection: 'column', alignItems: 'flex-start', gap: 16, + paddingBottom: 20, }); export const titleFont = style({ @@ -20,9 +21,14 @@ export const descriptionFont = style({ fontSize: 13, lineHeight: '18px', color: vars.color.font70, + marginBottom: 8, }); export const sectionFont = style({ fontWeight: 600, marginBottom: 0, }); + +export const descriptionContainer = style({ + marginBottom: 32, +}); diff --git a/src/renderer/Presentational/AddNode/AddNode.tsx b/src/renderer/Presentational/AddNode/AddNode.tsx deleted file mode 100644 index 0bd8daa88..000000000 --- a/src/renderer/Presentational/AddNode/AddNode.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -import { - container, - descriptionFont, - sectionFont, - titleFont, -} from './addNode.css'; -import SpecialSelect, { - SelectOption, -} from '../../Generics/redesign/SpecialSelect/SpecialSelect'; - -export interface AddNodeProps { - /** - * Listen to node config changes - */ - onChange: (newValue?: SelectOption) => void; -} - -const networksOptions = [ - { - value: 'ethereum', - label: 'Ethereum', - iconId: 'ethereum', - title: 'Ethereum', - info: 'The world computer', - }, -]; - -const AddNode = ({ onChange }: AddNodeProps) => { - const { t } = useTranslation(); - - return ( -
-
{t('AddYourFirstNode')}
-
- <>{t('AddYourFirstNodeDescription')} -
-

{t('ChooseYourNetwork')}

- onChange(selectedNetwork)} - /> -
- ); -}; - -export default AddNode; diff --git a/src/renderer/Presentational/AddNode/addNode.css.ts b/src/renderer/Presentational/AddNode/addNode.css.ts deleted file mode 100644 index 8b9c2aefc..000000000 --- a/src/renderer/Presentational/AddNode/addNode.css.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { vars } from '../../Generics/redesign/theme.css'; - -export const container = style({ - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - gap: 16, -}); - -export const titleFont = style({ - fontWeight: 500, - fontSize: 32, - lineHeight: '32px', - letterSpacing: '-0.01em', -}); - -export const descriptionFont = style({ - fontWeight: 400, - fontSize: 13, - lineHeight: '18px', - color: vars.color.font70, -}); - -export const sectionFont = style({ - fontWeight: 600, -}); diff --git a/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx b/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx index b959a2eed..24aa52804 100644 --- a/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx +++ b/src/renderer/Presentational/AddNodeStepper/AddNodeStepper.tsx @@ -2,6 +2,7 @@ // Just make sure to always render each child so that children component state isn't cleard import { useCallback, useEffect, useState } from 'react'; +import ContentWithSideArt from '../../Generics/redesign/ContentWithSideArt/ContentWithSideArt'; import { componentContainer, container } from './addNodeStepper.css'; import Stepper from '../../Generics/redesign/Stepper/Stepper'; import AddEthereumNode, { @@ -20,13 +21,18 @@ import { useAppDispatch } from '../../state/hooks'; import { NodeLibrary } from '../../../main/state/nodeLibrary'; // import { CheckStorageDetails } from '../../../main/files'; +import step1 from '../../assets/images/artwork/NN-Onboarding-Artwork-01.png'; +import step2 from '../../assets/images/artwork/NN-Onboarding-Artwork-02.png'; +import step3 from '../../assets/images/artwork/NN-Onboarding-Artwork-03.png'; + export interface AddNodeStepperProps { + modal?: boolean; onChange: (newValue: 'done' | 'cancel') => void; } const TOTAL_STEPS = 3; -const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => { +const AddNodeStepper = ({ onChange, modal = false }: AddNodeStepperProps) => { const dispatch = useAppDispatch(); const [sStep, setStep] = useState(0); // const [sExecutionClientLibrary, setExecutionClientLibrary] = useState< @@ -85,13 +91,13 @@ const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => { let ccReqs; if (newValue?.executionClient) { - const ecValue = newValue?.executionClient; + const ecValue = newValue?.executionClient.value; if (sNodeLibrary) { ecReqs = sNodeLibrary?.[ecValue]?.systemRequirements; } } if (newValue?.consensusClient) { - const ccValue = newValue?.consensusClient; + const ccValue = newValue?.consensusClient.value; if (sNodeLibrary) { ccReqs = sNodeLibrary?.[`${ccValue}-beacon`]?.systemRequirements; } @@ -125,13 +131,13 @@ const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => { if (sEthereumNodeConfig?.executionClient) { const ecValue = sEthereumNodeConfig?.executionClient; if (sNodeLibrary) { - ecNodeSpec = sNodeLibrary?.[ecValue]; + ecNodeSpec = sNodeLibrary?.[ecValue.value]; } } if (sEthereumNodeConfig?.consensusClient) { const ccValue = sEthereumNodeConfig?.consensusClient; if (sNodeLibrary) { - ccNodeSpec = sNodeLibrary?.[`${ccValue}-beacon`]; + ccNodeSpec = sNodeLibrary?.[`${ccValue.value}-beacon`]; } } console.log( @@ -186,36 +192,63 @@ const AddNodeStepper = ({ onChange }: AddNodeStepperProps) => { } }; - return ( -
-
- {/* Step 0 */} -
+ const getStepScreen = (step: number) => { + let stepScreen = null; + let stepImage = step1; + switch (step) { + case 0: + stepScreen = ( -
- - {/* Step 1 */} -
+ ); + stepImage = step1; + break; + case 1: + stepScreen = ( + ); + stepImage = step2; + break; + case 2: + stepScreen = ; + stepImage = step3; + break; + default: + } + + return ( + + {stepScreen} + + ); + }; + + return ( +
+
+ {/* Step 0 */} +
+ {getStepScreen(0)} +
+ + {/* Step 1 */} +
+ {getStepScreen(1)}
{/* Step 2 - If Docker is not installed */}
- + {getStepScreen(2)}
- -
- -
+
); }; diff --git a/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx b/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx new file mode 100644 index 000000000..44e603514 --- /dev/null +++ b/src/renderer/Presentational/AddNodeStepper/AddNodeStepperModal.tsx @@ -0,0 +1,164 @@ +// This component could be made into a Generic "FullScreenStepper" component +// Just make sure to always render each child so that children component state isn't cleard +import { useCallback, useEffect, useState } from 'react'; + +import { SelectOption } from '../../Generics/redesign/SpecialSelect/SpecialSelect'; +import { ModalConfig } from '../ModalManager/modalUtils'; +import ContentWithSideArt from '../../Generics/redesign/ContentWithSideArt/ContentWithSideArt'; +import { componentContainer, container } from './addNodeStepper.css'; +import AddEthereumNode, { + AddEthereumNodeValues, +} from '../AddEthereumNode/AddEthereumNode'; +import DockerInstallation from '../DockerInstallation/DockerInstallation'; +import NodeRequirements from '../NodeRequirements/NodeRequirements'; +import { SystemData } from '../../../main/systemInfo'; +import { SystemRequirements } from '../../../common/systemRequirements'; +import electron from '../../electronGlobal'; +import { mergeSystemRequirements } from './mergeNodeRequirements'; + +import step1 from '../../assets/images/artwork/NN-Onboarding-Artwork-01.png'; +import step2 from '../../assets/images/artwork/NN-Onboarding-Artwork-02.png'; +import step3 from '../../assets/images/artwork/NN-Onboarding-Artwork-03.png'; + +export interface AddNodeStepperModalProps { + modal?: boolean; + modalConfig: ModalConfig; + modalOnChangeConfig: (config: ModalConfig, save?: boolean) => void; + step: number; + disableSaveButton: (value: boolean) => void; +} + +const AddNodeStepperModal = ({ + modal = false, + modalConfig, + modalOnChangeConfig, + step, + disableSaveButton, +}: AddNodeStepperModalProps) => { + const [sEthereumNodeConfig, setEthereumNodeConfig] = + useState(); + const [sEthereumNodeRequirements, setEthereumNodeRequirements] = + useState(); + const [sSystemData, setSystemData] = useState(); + + const getData = async () => { + setSystemData(await electron.getSystemInfo()); + }; + + useEffect(() => { + getData(); + }, []); + + useEffect(() => { + const { + nodeLibrary, + consensusClient = 'nimbus', + executionClient = 'besu', + storageLocation, + } = modalConfig; + if (nodeLibrary && consensusClient && executionClient && storageLocation) { + const ecReqs = nodeLibrary?.[executionClient]?.systemRequirements; + const ccReqs = + nodeLibrary?.[`${consensusClient}-beacon`]?.systemRequirements; + try { + if (ecReqs && ccReqs) { + const mergedReqs = mergeSystemRequirements([ecReqs, ccReqs]); + console.log('mergedReqs', mergedReqs); + setEthereumNodeRequirements(mergedReqs); + } else { + throw new Error('ec or ec node requirements undefined'); + } + } catch (e) { + console.error(e); + } + } + }, [modalConfig]); + + const setConsensusClient = ( + clClient: SelectOption, + ethereumNodeConfig: AddEthereumNodeValues + ) => { + const config = { ...ethereumNodeConfig, consensusClient: clClient }; + modalOnChangeConfig({ + consensusClient: clClient.value, + }); + setEthereumNodeConfig(config); + }; + + const setExecutionClient = ( + elClient: SelectOption, + ethereumNodeConfig: AddEthereumNodeValues + ) => { + const config = { ...ethereumNodeConfig, executionClient: elClient }; + modalOnChangeConfig({ + executionClient: elClient.value, + }); + setEthereumNodeConfig(config); + }; + + const onChangeDockerInstall = useCallback((newValue: string) => { + console.log('onChangeDockerInstall newValue ', newValue); + if (newValue === 'done') { + disableSaveButton(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const getStepScreen = () => { + let stepScreen = null; + let stepImage = step1; + switch (step) { + case 0: + stepScreen = ( + + ); + stepImage = step1; + break; + case 1: + stepScreen = ( + + ); + stepImage = step2; + break; + case 2: + stepScreen = ( + + ); + stepImage = step3; + break; + default: + } + + return ( +
+ + {stepScreen} + +
+ ); + }; + + const modalStyle = modal ? 'modal' : ''; + + return ( +
+
{getStepScreen()}
+
+ ); +}; + +export default AddNodeStepperModal; diff --git a/src/renderer/Presentational/AddNodeStepper/addNodeStepper.css.ts b/src/renderer/Presentational/AddNodeStepper/addNodeStepper.css.ts index 765c65571..fbd796ecb 100644 --- a/src/renderer/Presentational/AddNodeStepper/addNodeStepper.css.ts +++ b/src/renderer/Presentational/AddNodeStepper/addNodeStepper.css.ts @@ -6,12 +6,17 @@ export const container = style({ width: '100%', display: 'flex', flexDirection: 'column', + selectors: { + [`&.modal`]: { + width: 560, + }, + }, }); export const componentContainer = style({ width: '100%', flexGrow: 1, - overflow: 'auto', + overflow: 'visible', boxSizing: 'border-box', }); diff --git a/src/renderer/Presentational/ContentSingleClient/ContentSingleClient.tsx b/src/renderer/Presentational/ContentSingleClient/ContentSingleClient.tsx index 21f244fe5..6055aabf8 100644 --- a/src/renderer/Presentational/ContentSingleClient/ContentSingleClient.tsx +++ b/src/renderer/Presentational/ContentSingleClient/ContentSingleClient.tsx @@ -7,7 +7,6 @@ import { HorizontalLine } from '../../Generics/redesign/HorizontalLine/Horizonta import { HeaderMetrics } from '../../Generics/redesign/HeaderMetrics/HeaderMetrics'; import { Header } from '../../Generics/redesign/Header/Header'; // import LabelValues from '../../Generics/redesign/LabelValues/LabelValues'; -import { container } from './contentSingleClient.css'; import { NodeAction, NodeOverviewProps } from '../../Generics/redesign/consts'; // TODO: process retrieved client data into this format @@ -62,7 +61,7 @@ const ContentSingleClient = (props: SingleNodeContent) => { // TODO: retrieve initial data for all pages return ( -
+ <> {/* todo: fix temp type casting */}
@@ -85,7 +84,7 @@ const ContentSingleClient = (props: SingleNodeContent) => {
-
+ ); }; export default ContentSingleClient; diff --git a/src/renderer/Presentational/ContentSingleClient/contentSingleClient.css.ts b/src/renderer/Presentational/ContentSingleClient/contentSingleClient.css.ts index 713f14dbe..d921a4173 100644 --- a/src/renderer/Presentational/ContentSingleClient/contentSingleClient.css.ts +++ b/src/renderer/Presentational/ContentSingleClient/contentSingleClient.css.ts @@ -1,11 +1,6 @@ import { style } from '@vanilla-extract/css'; import { vars } from '../../Generics/redesign/theme.css'; -export const container = style({ - margin: '64px 40px', - boxSizing: 'border-box', -}); - export const sectionTitle = style({ fontWeight: 590, fontSize: '20px', diff --git a/src/renderer/Presentational/DockerInstallation/DockerInstallation.tsx b/src/renderer/Presentational/DockerInstallation/DockerInstallation.tsx index bf895a1a0..652223c18 100644 --- a/src/renderer/Presentational/DockerInstallation/DockerInstallation.tsx +++ b/src/renderer/Presentational/DockerInstallation/DockerInstallation.tsx @@ -18,8 +18,6 @@ import { useGetIsDockerInstalledQuery, useGetIsDockerRunningQuery, } from '../../state/settingsService'; -import ContentWithSideArt from '../../Generics/redesign/ContentWithSideArt/ContentWithSideArt'; -import graphicsPng from '../../assets/images/artwork/NN-Onboarding-Artwork-03.png'; // 6.5 min on 2022 MacbookPro 16inch, baseline const TOTAL_INSTALL_TIME_SEC = 7 * 60; @@ -28,9 +26,15 @@ export interface DockerInstallationProps { * Listen to node config changes */ onChange: (newValue: string) => void; + disableSaveButton?: (newValue: boolean) => void; + type?: string; } -const DockerInstallation = ({ onChange }: DockerInstallationProps) => { +const DockerInstallation = ({ + onChange, + disableSaveButton, + type = '', +}: DockerInstallationProps) => { const { t } = useTranslation(); const qIsDockerInstalled = useGetIsDockerInstalledQuery(); const isDockerInstalled = qIsDockerInstalled?.data; @@ -45,6 +49,11 @@ const DockerInstallation = ({ onChange }: DockerInstallationProps) => { const [sDownloadedBytes, setDownloadedBytes] = useState(0); const [sInstallComplete, setInstallComplete] = useState(); + useEffect(() => { + if (disableSaveButton) disableSaveButton(true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { if (isDockerRunning) { onChange('done'); @@ -102,68 +111,68 @@ const DockerInstallation = ({ onChange }: DockerInstallationProps) => { // listen to docker install messages return ( - -
+
+ {type !== 'modal' && (
{t('DockerInstallation')}
-
- <>{t('dockerPurpose')} -
- - {/* Docker is not installed */} - {!isDockerInstalled && ( - <> - {!sDownloadComplete && !sInstallComplete && ( - <> - {!sHasStartedDownload ? ( -
-
- ) : ( - + <>{t('dockerPurpose')} +
+ + {/* Docker is not installed */} + {!isDockerInstalled && ( + <> + {!sDownloadComplete && !sInstallComplete && ( + <> + {!sHasStartedDownload ? ( +
+
+
+ ) : ( + + )} + + )} + {sDownloadComplete && !sInstallComplete && ( + -
{t('DockerUncheckOpenAtStartup')}
- - )} - {isDockerRunning && <>{t('DockerIsRunningProceed')}} -
- + )} + + )} + + {sDownloadComplete && sInstallComplete && ( +

{t('DockerInstallComplete')}

+ )} + {/* Docker is installed but not running */} + {isDockerInstalled && !isDockerRunning && ( + <> +
); }; diff --git a/src/renderer/Presentational/DockerInstallation/dockerInstallation.css.ts b/src/renderer/Presentational/DockerInstallation/dockerInstallation.css.ts index 14e6e043e..12c59f600 100644 --- a/src/renderer/Presentational/DockerInstallation/dockerInstallation.css.ts +++ b/src/renderer/Presentational/DockerInstallation/dockerInstallation.css.ts @@ -6,6 +6,12 @@ export const container = style({ flexDirection: 'column', alignItems: 'flex-start', gap: 16, + paddingBottom: 15, + selectors: { + '&.modal': { + width: 560, + }, + }, }); export const titleFont = style({ diff --git a/src/renderer/Presentational/ModalManager/AddNodeModal.tsx b/src/renderer/Presentational/ModalManager/AddNodeModal.tsx new file mode 100644 index 000000000..52a68dd3a --- /dev/null +++ b/src/renderer/Presentational/ModalManager/AddNodeModal.tsx @@ -0,0 +1,132 @@ +import { useState, useCallback } from 'react'; +import electron from '../../electronGlobal'; +import { useAppDispatch } from '../../state/hooks'; +import { updateSelectedNodeId } from '../../state/node'; +import AddNodeStepperModal from '../AddNodeStepper/AddNodeStepperModal'; +import { Modal } from '../../Generics/redesign/Modal/Modal'; +import { modalOnChangeConfig, ModalConfig } from './modalUtils'; +import { useGetIsDockerRunningQuery } from '../../state/settingsService'; + +type Props = { + modalOnClose: () => void; +}; + +export const AddNodeModal = ({ modalOnClose }: Props) => { + const [modalConfig, setModalConfig] = useState({}); + const [isSaveButtonDisabled, setIsSaveButtonDisabled] = + useState(false); + const [step, setStep] = useState(0); + + const qIsDockerRunning = useGetIsDockerRunningQuery(null, { + pollingInterval: 15000, + }); + const isDockerRunning = qIsDockerRunning?.data; + + const dispatch = useAppDispatch(); + + let modalTitle = ''; + switch (step) { + case 0: + modalTitle = 'Launch an Ethereum Node'; + break; + case 1: + modalTitle = 'Node Requirements'; + break; + case 2: + modalTitle = 'Docker Installation'; + break; + default: + } + + const startNode = (step === 1 || step === 2) && isDockerRunning; + const buttonSaveLabel = startNode ? 'Start node' : 'Continue'; + const buttonCancelLabel = step === 0 ? 'Cancel' : 'Back'; + const buttonSaveVariant = startNode ? 'icon-left' : 'text'; + + const modalOnSaveConfig = async (updatedConfig: ModalConfig | undefined) => { + const { + executionClient = 'besu', + consensusClient = 'nimbus', + storageLocation, + nodeLibrary, + } = updatedConfig || (modalConfig as ModalConfig); + + let ecNodeSpec; + let ccNodeSpec; + if (nodeLibrary) { + ecNodeSpec = nodeLibrary?.[executionClient]; + ccNodeSpec = nodeLibrary?.[`${consensusClient}-beacon`]; + } + + if (!ecNodeSpec || !ccNodeSpec) { + throw new Error('ecNodeSpec or ccNodeSpec is undefined'); + } + + // eslint-disable-next-line no-case-declarations + const { ecNode, ccNode } = await electron.addEthereumNode( + ecNodeSpec, + ccNodeSpec, + { storageLocation } + ); + + dispatch(updateSelectedNodeId(ecNode.id)); + await electron.startNode(ecNode.id); + await electron.startNode(ccNode.id); + }; + + const disableSaveButton = useCallback((value: boolean) => { + setIsSaveButtonDisabled(value); + }, []); + + const onCancel = () => { + if (step === 0) { + modalOnClose(); + } else if (step === 1) { + setStep(0); + } else { + setStep(1); + } + }; + + const onSave = () => { + if (step === 0) { + setStep(1); + } else if (step === 1 && !isDockerRunning) { + setStep(2); + } else { + modalOnSaveConfig(undefined); + modalOnClose(); + } + }; + + return ( + + { + modalOnChangeConfig( + config, + modalConfig, + setModalConfig, + save, + modalOnSaveConfig + ); + }} + disableSaveButton={disableSaveButton} + /> + + ); +}; diff --git a/src/renderer/Presentational/ModalManager/ModalManager.tsx b/src/renderer/Presentational/ModalManager/ModalManager.tsx new file mode 100644 index 000000000..43f863af6 --- /dev/null +++ b/src/renderer/Presentational/ModalManager/ModalManager.tsx @@ -0,0 +1,54 @@ +import { useSelector } from 'react-redux'; +import { useCallback } from 'react'; +import { useAppDispatch } from '../../state/hooks'; +import { getModalState, setModalState } from '../../state/modal'; +import { modalRoutes } from './modalUtils'; +import { NodeSettingsModal } from './NodeSettingsModal'; +import { PreferencesModal } from './PreferencesModal'; +import { RemoveNodeModal } from './RemoveNodeModal'; +import { AddNodeModal } from './AddNodeModal'; + +const ModalManager = () => { + const { isModalOpen, screen } = useSelector(getModalState); + const dispatch = useAppDispatch(); + + const modalOnClose = useCallback(() => { + dispatch( + setModalState({ + isModalOpen: false, + screen: { route: undefined, type: undefined }, + }) + ); + }, [dispatch]); + + if (!isModalOpen) { + return null; + } + + // Render the appropriate screen based on the current `screen` value + switch (screen.route) { + // Modals + case modalRoutes.addNode: + return ; + case modalRoutes.nodeSettings: + return ; + case modalRoutes.preferences: + return ; + case modalRoutes.addValidator: + return null; + case modalRoutes.clientVersions: + return null; + + // Alerts + case modalRoutes.stopNode: + return null; + case modalRoutes.removeNode: + return ; + case modalRoutes.updateUnavailable: + return null; + default: + return null; + } +}; + +export default ModalManager; diff --git a/src/renderer/Presentational/ModalManager/NodeSettingsModal.tsx b/src/renderer/Presentational/ModalManager/NodeSettingsModal.tsx new file mode 100644 index 000000000..07c3cd220 --- /dev/null +++ b/src/renderer/Presentational/ModalManager/NodeSettingsModal.tsx @@ -0,0 +1,64 @@ +import { useTranslation } from 'react-i18next'; +import { useState, useCallback } from 'react'; +import electron from '../../electronGlobal'; +import NodeSettingsWrapper from '../NodeSettings/NodeSettingsWrapper'; +import { Modal } from '../../Generics/redesign/Modal/Modal'; +import { modalOnChangeConfig, ModalConfig } from './modalUtils'; + +type Props = { + modalOnClose: () => void; +}; + +export const NodeSettingsModal = ({ modalOnClose }: Props) => { + const [modalConfig, setModalConfig] = useState({}); + const [isSaveButtonDisabled, setIsSaveButtonDisabled] = useState(false); + const { t } = useTranslation('genericComponents'); + const modalTitle = t('NodeSettings'); + const buttonSaveLabel = 'Save changes'; + + const modalOnSaveConfig = async (updatedConfig: ModalConfig | undefined) => { + const { settingsConfig, selectedNode, newDataDir } = + updatedConfig || (modalConfig as ModalConfig); + + if (settingsConfig && selectedNode) { + await electron.updateNode(selectedNode.id, { + config: settingsConfig, + }); + } + if (newDataDir && selectedNode) { + await electron.updateNodeDataDir(selectedNode, newDataDir); + } + }; + + const disableSaveButton = useCallback((value: boolean) => { + setIsSaveButtonDisabled(value); + }, []); + + return ( + { + modalOnSaveConfig(undefined); + modalOnClose(); + }} + modalOnClose={modalOnClose} + modalOnCancel={modalOnClose} + isSaveButtonDisabled={isSaveButtonDisabled} + > + { + modalOnChangeConfig( + config, + modalConfig, + setModalConfig, + save, + modalOnSaveConfig + ); + }} + disableSaveButton={disableSaveButton} + /> + + ); +}; diff --git a/src/renderer/Presentational/ModalManager/PreferencesModal.tsx b/src/renderer/Presentational/ModalManager/PreferencesModal.tsx new file mode 100644 index 000000000..f92b1c0c8 --- /dev/null +++ b/src/renderer/Presentational/ModalManager/PreferencesModal.tsx @@ -0,0 +1,66 @@ +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import { ThemeSetting } from 'main/state/settings'; +import electron from '../../electronGlobal'; +import PreferencesWrapper from '../Preferences/PreferencesWrapper'; +import { Modal } from '../../Generics/redesign/Modal/Modal'; +import { modalOnChangeConfig, ModalConfig } from './modalUtils'; + +type Props = { + modalOnClose: () => void; +}; + +interface MetaElement extends HTMLMetaElement { + content: ThemeSetting; +} + +export const PreferencesModal = ({ modalOnClose }: Props) => { + const [modalConfig, setModalConfig] = useState({}); + const { t } = useTranslation('genericComponents'); + const modalTitle = t('Preferences'); + const buttonSaveLabel = 'Save changes'; + + const handleColorSchemeChange = (colorScheme: ThemeSetting) => { + const meta = document.querySelector( + 'meta[name="color-scheme"]' + ) as MetaElement; + const colorValue = colorScheme === 'auto' ? 'light dark' : colorScheme; + meta.content = colorValue as ThemeSetting; + }; + + const modalOnSaveConfig = async (updatedConfig: ModalConfig | undefined) => { + const { theme, isOpenOnStartup } = + updatedConfig || (modalConfig as ModalConfig); + + if (theme) { + await electron.setThemeSetting(theme); + handleColorSchemeChange(theme); + } + if (isOpenOnStartup) { + await electron.setIsOpenOnStartup(isOpenOnStartup); + } + modalOnClose(); + }; + + return ( + + { + modalOnChangeConfig( + config, + modalConfig, + setModalConfig, + save, + modalOnSaveConfig + ); + }} + /> + + ); +}; diff --git a/src/renderer/Presentational/ModalManager/RemoveNodeModal.tsx b/src/renderer/Presentational/ModalManager/RemoveNodeModal.tsx new file mode 100644 index 000000000..97079d920 --- /dev/null +++ b/src/renderer/Presentational/ModalManager/RemoveNodeModal.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import { setModalState } from '../../state/modal'; +import electron from '../../electronGlobal'; +import { useAppDispatch } from '../../state/hooks'; +import RemoveNodeWrapper from '../RemoveNodeModal/RemoveNodeWrapper'; +import { Modal } from '../../Generics/redesign/Modal/Modal'; +import { modalOnChangeConfig, ModalConfig } from './modalUtils'; + +type Props = { + modalOnClose: () => void; +}; + +export const RemoveNodeModal = ({ modalOnClose }: Props) => { + const [modalConfig, setModalConfig] = useState({}); + const dispatch = useAppDispatch(); + const modalTitle = 'Are you sure you want to remove this node?'; + const buttonSaveLabel = 'Remove node'; + const buttonSaveType = 'danger'; + + const modalOnSaveConfig = async (updatedConfig: ModalConfig | undefined) => { + const { selectedNode, isDeleteStorage = true } = + updatedConfig || (modalConfig as ModalConfig); + + try { + if (selectedNode) { + await electron.removeNode(selectedNode.id, { + isDeleteStorage, + }); + } + } catch (err) { + console.error(err); + throw new Error( + 'There was an error removing the node. Try again and please report the error to the NiceNode team in Discord.' + ); + } + modalOnClose(); + }; + + const onCancel = () => { + dispatch( + setModalState({ + isModalOpen: true, + screen: { route: 'nodeSettings', type: 'modal' }, + }) + ); + }; + + return ( + + { + modalOnChangeConfig( + config, + modalConfig, + setModalConfig, + save, + modalOnSaveConfig + ); + }} + /> + + ); +}; diff --git a/src/renderer/Presentational/ModalManager/modalUtils.tsx b/src/renderer/Presentational/ModalManager/modalUtils.tsx new file mode 100644 index 000000000..5a174947c --- /dev/null +++ b/src/renderer/Presentational/ModalManager/modalUtils.tsx @@ -0,0 +1,63 @@ +import { ThemeSetting } from 'main/state/settings'; +import React from 'react'; +import Node from 'common/node'; + +export interface ModalConfig { + executionClient?: string; + consensusClient?: string; + storageLocation?: string; + theme?: ThemeSetting; + isOpenOnStartup?: boolean; + selectedNode?: Node; + isDeleteStorage?: boolean; + settingsConfig?: object; + newDataDir?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +export const modalRoutes = Object.freeze({ + addNode: 'addNode', + nodeSettings: 'nodeSettings', + preferences: 'preferences', + addValidator: 'addValidator', + clientVersions: 'clientVersions', + stopNode: 'stopNode', + removeNode: 'removeNode', + updateUnavailable: 'updateUnavailable', +}); + +/* Use this to change config settings, saved temporarily in the modal file with backend apis until it's saved by modalOnSaveConfig +You can also pass in a save flag to update the config immediately with the temporarily saved config settings +This should always be called in the XXModal.tsx file due to needing access to setModalConfig, and current modalConfig, and passed into appropriate Wrapper file */ +export const modalOnChangeConfig = async ( + config: ModalConfig, + modalConfig: ModalConfig, + setModalConfig: React.Dispatch>, + save?: boolean, + modalOnSaveConfig?: (config: ModalConfig) => Promise +) => { + if (!setModalConfig || !modalConfig) { + throw new Error('modal config is not defined'); + } + + let updatedConfig = {}; + const keys = Object.keys(config); + if (keys.length > 1) { + updatedConfig = { + ...modalConfig, + ...config, + }; + } else { + const key = keys[0]; + updatedConfig = { + ...modalConfig, + [key]: config[key], + }; + } + setModalConfig(updatedConfig); + + if (save && modalOnSaveConfig) { + await modalOnSaveConfig(updatedConfig); + } +}; diff --git a/src/renderer/Presentational/NodeLogsModal/LogsModal.tsx b/src/renderer/Presentational/NodeLogsModal/LogsModal.tsx deleted file mode 100644 index bb01be5e8..000000000 --- a/src/renderer/Presentational/NodeLogsModal/LogsModal.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import electron from '../../electronGlobal'; -import { LogWithMetadata } from '../../../main/util/nodeLogUtils'; -import { useAppSelector } from '../../state/hooks'; -import { selectSelectedNodeId } from '../../state/node'; - -import { Logs } from '../../Generics/redesign/LogMessage/Logs'; -import { Modal } from '../../Generics/redesign/Modal/Modal'; - -export type ThemeSetting = 'light' | 'dark' | 'auto'; -export type Preference = 'theme' | 'isOpenOnStartup'; -export interface LogsModalProps { - isOpen: boolean; - onClickClose: () => void; -} - -const LogsModal = ({ isOpen, onClickClose }: LogsModalProps) => { - const sSelectedNodeId = useAppSelector(selectSelectedNodeId); - const [sLogs, setLogs] = useState([]); - - const nodeLogsListener = (message: LogWithMetadata[]) => { - setLogs((prevState) => { - if (prevState.length < 1000) { - return [...prevState, message[0]]; - } - return [message[0]]; - }); - }; - - const listenForNodeLogs = useCallback(async () => { - electron.ipcRenderer.on('nodeLogs', nodeLogsListener); - }, []); - - useEffect(() => { - if (isOpen) { - console.log('LogsModal: isOpen. Listening for logs.'); - listenForNodeLogs(); - } else { - setLogs([]); - console.log('LogsModal: isclosed. Clear logs and removeAllListeners.'); - electron.ipcRenderer.removeAllListeners('nodeLogs'); - } - return () => electron.ipcRenderer.removeAllListeners('nodeLogs'); - }, [isOpen, listenForNodeLogs]); - - useEffect(() => { - // when switching selected nodes... - // if none selected, send stop - // if one is selected, ask for those logs - setLogs([]); - console.log('LogsModal: isOpen, sSelectedNodeId changed. Clear logs.'); - if (isOpen && sSelectedNodeId) { - electron.sendNodeLogs(sSelectedNodeId); - console.log( - 'LogsModal: isOpen && sSelectedNodeId truthy. Send selected node logs' - ); - } else { - console.log( - 'LogsModal: isOpen && sSelectedNodeId falsy. stopSendingNodeLogs' - ); - electron.stopSendingNodeLogs(); - } - return () => { - electron.stopSendingNodeLogs(); - }; - }, [isOpen, sSelectedNodeId]); - - return ( - - - - ); -}; - -export default LogsModal; diff --git a/src/renderer/Presentational/NodeRequirements/NodeRequirements.tsx b/src/renderer/Presentational/NodeRequirements/NodeRequirements.tsx index 43b18f17a..9f0e6bf08 100644 --- a/src/renderer/Presentational/NodeRequirements/NodeRequirements.tsx +++ b/src/renderer/Presentational/NodeRequirements/NodeRequirements.tsx @@ -9,8 +9,6 @@ import { SystemRequirements } from '../../../common/systemRequirements'; // eslint-disable-next-line import/no-cycle import { makeCheckList } from './requirementsChecklistUtil'; import ExternalLink from '../../Generics/redesign/Link/ExternalLink'; -import ContentWithSideArt from '../../Generics/redesign/ContentWithSideArt/ContentWithSideArt'; -import graphicsPng from '../../assets/images/artwork/NN-Onboarding-Artwork-02.png'; export interface NodeRequirementsProps { /** @@ -25,12 +23,14 @@ export interface NodeRequirementsProps { * A folder path where the node data will be stored. */ nodeStorageLocation?: string; + type?: string; } const NodeRequirements = ({ nodeRequirements, systemData, nodeStorageLocation, + type, }: NodeRequirementsProps) => { const { t } = useTranslation('systemRequirements'); const [sItems, setItems] = useState([]); @@ -58,26 +58,24 @@ const NodeRequirements = ({ }, [nodeRequirements, systemData, nodeStorageLocation]); return ( - -
-
Node Requirements
-
- {nodeRequirements?.description ? ( - nodeRequirements.description - ) : ( - <>{t('nodeRequirementsDefaultDescription')} - )} -
- {nodeRequirements?.documentationUrl && ( - +
+ {type !== 'modal' &&
Node Requirements
} +
+ {nodeRequirements?.description ? ( + nodeRequirements.description + ) : ( + <>{t('nodeRequirementsDefaultDescription')} )} - {!nodeRequirements && <>{t('nodeRequirementsUnavailable')}} -
- + {nodeRequirements?.documentationUrl && ( + + )} + {!nodeRequirements && <>{t('nodeRequirementsUnavailable')}} + +
); }; diff --git a/src/renderer/Presentational/NodeScreen/NodeScreen.css.ts b/src/renderer/Presentational/NodeScreen/NodeScreen.css.ts new file mode 100644 index 000000000..7f3d40ca0 --- /dev/null +++ b/src/renderer/Presentational/NodeScreen/NodeScreen.css.ts @@ -0,0 +1,36 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '../../Generics/redesign/theme.css'; + +export const container = style({ + width: '100%', + height: '100%', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', +}); + +export const contentContainer = style({ + alignItems: 'center', + display: 'flex', + flexDirection: 'column', + width: 520, +}); + +export const titleFont = style({ + fontWeight: 590, + fontSize: 15, + lineHeight: '20px', + color: vars.color.font, + letterSpacing: '-0.24px', + textAlign: 'center', + marginBottom: 8, +}); + +export const descriptionFont = style({ + fontWeight: 400, + fontSize: 13, + lineHeight: '18px', + color: vars.color.font70, + letterSpacing: '-0.08px', + marginBottom: 24, +}); diff --git a/src/renderer/NodeScreen.tsx b/src/renderer/Presentational/NodeScreen/NodeScreen.tsx similarity index 81% rename from src/renderer/NodeScreen.tsx rename to src/renderer/Presentational/NodeScreen/NodeScreen.tsx index 352f2a199..8483a51ba 100644 --- a/src/renderer/NodeScreen.tsx +++ b/src/renderer/Presentational/NodeScreen/NodeScreen.tsx @@ -2,25 +2,33 @@ import { useCallback, useEffect, useState } from 'react'; // import { NodeStatus } from '../common/node'; -import electron from './electronGlobal'; +import { setModalState } from '../../state/modal'; +import electron from '../../electronGlobal'; // import { useGetNodesQuery } from './state/nodeService'; -import { useAppSelector } from './state/hooks'; -import { selectIsAvailableForPolling, selectSelectedNode } from './state/node'; +import { useAppSelector, useAppDispatch } from '../../state/hooks'; +import { + selectIsAvailableForPolling, + selectSelectedNode, +} from '../../state/node'; import { useGetExecutionIsSyncingQuery, useGetExecutionLatestBlockQuery, useGetExecutionPeersQuery, useGetNodeVersionQuery, -} from './state/services'; +} from '../../state/services'; // import { useGetNetworkConnectedQuery } from './state/network'; import ContentSingleClient, { SingleNodeContent, -} from './Presentational/ContentSingleClient/ContentSingleClient'; -import { hexToDecimal } from './utils'; -import { NodeAction } from './Generics/redesign/consts'; -import NNSplash from './Presentational/NNSplashScreen/NNSplashScreen'; -import AddNodeStepper from './Presentational/AddNodeStepper/AddNodeStepper'; -import { Modal } from './Generics/redesign/Modal/Modal'; +} from '../ContentSingleClient/ContentSingleClient'; +import { hexToDecimal } from '../../utils'; +import { NodeAction } from '../../Generics/redesign/consts'; +import Button from '../../Generics/redesign/Button/Button'; +import { + container, + contentContainer, + titleFont, + descriptionFont, +} from './NodeScreen.css'; const NodeScreen = () => { // const { t } = useTranslation(); @@ -54,7 +62,6 @@ const NodeScreen = () => { pollingInterval, } ); - const [sIsModalOpenAddNode, setIsModalOpenAddNode] = useState(); // use to show if internet is disconnected // const qNetwork = useGetNetworkConnectedQuery(null, { @@ -165,30 +172,34 @@ const NodeScreen = () => { // }; // }, // }); + const dispatch = useAppDispatch(); if (!selectedNode) { // if there is no node selected, prompt user to create a new node return ( - <> - {!sIsModalOpenAddNode && ( - setIsModalOpenAddNode(true)} /> - )} - {/* Todo: remove this when Modal Manager is created */} - setIsModalOpenAddNode(false)} - isFullScreen - > - { - console.log(newValue); - if (newValue === 'done' || newValue === 'cancel') { - setIsModalOpenAddNode(false); - } +
+
+
No active nodes
+
+ Add your first node and start verifying the validty of every block + of your favourite blockchain. Running a node also helps others to + download and update their copies. +
+
+
); } diff --git a/src/renderer/Presentational/NodeSettings/NodeSettings.css.ts b/src/renderer/Presentational/NodeSettings/NodeSettings.css.ts new file mode 100644 index 000000000..aef6d4500 --- /dev/null +++ b/src/renderer/Presentational/NodeSettings/NodeSettings.css.ts @@ -0,0 +1,19 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '../../Generics/redesign/theme.css'; + +export const nodeCommandTitle = style({ + fontWeight: 590, + color: vars.color.font70, + letterSpacing: '-0.12px', + fontSize: '13px', + lineHeight: '16px', +}); + +export const nodeCommandContainer = style({ + display: 'flex', + paddingTop: 8, +}); + +export const nodeCommand = style({ + fontFamily: 'monospace', +}); diff --git a/src/renderer/Presentational/NodeSettingsModal/NodeSettingsModal.tsx b/src/renderer/Presentational/NodeSettings/NodeSettings.tsx similarity index 75% rename from src/renderer/Presentational/NodeSettingsModal/NodeSettingsModal.tsx rename to src/renderer/Presentational/NodeSettings/NodeSettings.tsx index 82f7832e4..b4881d7d8 100644 --- a/src/renderer/Presentational/NodeSettingsModal/NodeSettingsModal.tsx +++ b/src/renderer/Presentational/NodeSettings/NodeSettings.tsx @@ -1,5 +1,4 @@ import { useTranslation } from 'react-i18next'; -import { Modal } from '../../Generics/redesign/Modal/Modal'; import DynamicSettings, { CategoryConfig, } from '../../Generics/redesign/DynamicSettings/DynamicSettings'; @@ -10,12 +9,15 @@ import InternalLink from '../../Generics/redesign/Link/InternalLink'; import { WalletSettings } from './WalletSettings'; import { Tabs } from '../../Generics/redesign/Tabs/Tabs'; import Button from '../../Generics/redesign/Button/Button'; +import { + nodeCommandTitle, + nodeCommandContainer, + nodeCommand, +} from './NodeSettings.css'; export type ThemeSetting = 'light' | 'dark' | 'auto'; export type Preference = 'theme' | 'isOpenOnStartup'; export interface NodeSettingsProps { - isOpen: boolean; - onClickClose: () => void; categoryConfigs?: CategoryConfig[]; configValuesMap?: ConfigValuesMap; httpCorsConfigTranslation?: ConfigTranslation; @@ -27,8 +29,6 @@ export interface NodeSettingsProps { } const NodeSettings = ({ - isOpen, - onClickClose, categoryConfigs, configValuesMap, httpCorsConfigTranslation, @@ -40,6 +40,10 @@ const NodeSettings = ({ }: NodeSettingsProps) => { const { t: tNiceNode } = useTranslation(); + if (!categoryConfigs || categoryConfigs.length === 0) { + return null; + } + const renderTabs = () => { const tabs = []; tabs.push( @@ -52,26 +56,31 @@ const NodeSettings = ({ )} {/* todo: tab1 */} -

Node start command

{nodeStartCommand && ( -
-

{nodeStartCommand}

-
+ <> +

+ Node start command (must save changes to take effect) +

+
+

{nodeStartCommand}

+
+ )} {/* Remove Node link */}
@@ -98,15 +107,7 @@ const NodeSettings = ({ return tabs; }; - return ( - - {renderTabs()} - - ); + return {renderTabs()}; }; export default NodeSettings; diff --git a/src/renderer/Presentational/NodeSettingsModal/NodeSettingsWrapper.tsx b/src/renderer/Presentational/NodeSettings/NodeSettingsWrapper.tsx similarity index 54% rename from src/renderer/Presentational/NodeSettingsModal/NodeSettingsWrapper.tsx rename to src/renderer/Presentational/NodeSettings/NodeSettingsWrapper.tsx index 980f169f9..05fd60488 100644 --- a/src/renderer/Presentational/NodeSettingsModal/NodeSettingsWrapper.tsx +++ b/src/renderer/Presentational/NodeSettings/NodeSettingsWrapper.tsx @@ -1,5 +1,7 @@ -import { useCallback, useEffect, useState } from 'react'; -import { NodeId } from '../../../common/node'; +import { useEffect, useState } from 'react'; +import { ModalConfig } from '../ModalManager/modalUtils'; +import { setModalState } from '../../state/modal'; +import Node, { NodeId } from '../../../common/node'; import { ConfigTranslation, ConfigTranslationMap, @@ -8,42 +10,93 @@ import { } from '../../../common/nodeConfig'; import electron from '../../electronGlobal'; import { CategoryConfig } from '../../Generics/redesign/DynamicSettings/DynamicSettings'; -import { useAppSelector } from '../../state/hooks'; +import { useAppDispatch, useAppSelector } from '../../state/hooks'; import { selectSelectedNode } from '../../state/node'; -import RemoveNodeWrapper, { - RemoveNodeAction, -} from '../RemoveNodeModal/RemoveNodeWrapper'; -import NodeSettings from './NodeSettingsModal'; +import NodeSettings from './NodeSettings'; export type SettingChangeHandler = ( configKey: string, newValue: ConfigValue ) => void; export interface NodeSettingsWrapperProps { - isOpen: boolean; - onClickClose: () => void; + modalOnChangeConfig: (config: ModalConfig, save?: true) => void; + disableSaveButton: (value: boolean) => void; } const HTTP_CORS_DOMAINS_KEY = 'httpCorsDomains'; const NodeSettingsWrapper = ({ - isOpen, - onClickClose, + modalOnChangeConfig, + disableSaveButton, }: NodeSettingsWrapperProps) => { const [sIsConfigDisabled, setIsConfigDisabled] = useState(true); const [sConfigTranslationMap, setConfigTranslationMap] = useState(); const [sCategoryConfigs, setCategoryConfigs] = useState(); - const [sIsRemoveNodeModalOpen, setIsRemoveNodeModalOpen] = - useState(false); const [sIsWalletSettingsEnabled, setIsWalletSettingsEnabled] = useState(false); // find httpCors config translation for the wallet settings tab const [sHttpCorsConfigTranslation, setHttpCorsConfigTranslation] = useState(); const [sNodeStartCommand, setNodeStartCommand] = useState(); + const sSelectedNode = useAppSelector(selectSelectedNode); + const [selectedNode, setSelectedNode] = useState( + sSelectedNode + ); + const [initialized, setInitialized] = useState(false); + + const dispatch = useAppDispatch(); + + const getUpdatedConfigValuesMap = () => { + if (selectedNode?.config && sConfigTranslationMap) { + const { configValuesMap } = selectedNode.config; + const keysToIgnore = [ + 'dataDir', + 'http' /* add any other keys to ignore here */, + ]; + + const newConfigValuesMap: { + [key: string]: string | string[] | undefined; + } = { ...configValuesMap }; + + Object.keys(sConfigTranslationMap) + .filter( + (key) => !keysToIgnore.includes(key) && !(key in newConfigValuesMap) + ) + .forEach((key) => { + newConfigValuesMap[key] = + sConfigTranslationMap[key]?.defaultValue || ''; + }); + return newConfigValuesMap; + } + return null; + }; + + const updatedConfig = getUpdatedConfigValuesMap(); - const selectedNode = useAppSelector(selectSelectedNode); + useEffect(() => { + if (selectedNode?.config && sConfigTranslationMap && !initialized) { + const { configValuesMap } = selectedNode.config; + + // check if there are any new keys in configTranslations compared to config values + if ( + Object.keys(configValuesMap).length === + Object.keys(sConfigTranslationMap).length + ) { + setInitialized(true); + return; + } + + const newConfig = { + ...selectedNode.config, + configValuesMap: updatedConfig, + }; + + modalOnChangeConfig({ settingsConfig: newConfig, selectedNode }, true); + setInitialized(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sCategoryConfigs, sConfigTranslationMap, selectedNode, initialized]); useEffect(() => { let isDisabled = true; @@ -56,10 +109,13 @@ const NodeSettingsWrapper = ({ isWalletSettingsEnabled = selectedNode.spec.category === 'L1/ExecutionClient'; } + if (isDisabled) { + disableSaveButton(true); + } setIsConfigDisabled(isDisabled); setConfigTranslationMap(configTranslationMap); setIsWalletSettingsEnabled(isWalletSettingsEnabled); - }, [selectedNode]); + }, [disableSaveButton, selectedNode]); // configTranslationMap = selectedNode.spec.configTranslation; useEffect(() => { @@ -126,33 +182,61 @@ const NodeSettingsWrapper = ({ ) => { // updateNode console.log('updating node with newValue: ', newValue); - if (selectedNode?.config) { + if (selectedNode?.config && updatedConfig) { // If the configChange is for a folder location, open electron - const { configValuesMap } = selectedNode.config; - const currentValue = configValuesMap[configKey]; + const currentValue = updatedConfig[configKey]; const { configTranslation } = selectedNode.spec; if ( configTranslation && configTranslation[configKey]?.uiControl.type === FilePathControlType ) { - const openDialogForNodeDataDir = - await electron.openDialogForNodeDataDir(selectedNode.id); + const newDataDir = await electron.openDialogForNodeDataDir( + selectedNode.id + ); console.log( 'openDialogForNodeDataDir before, and res:', currentValue, - openDialogForNodeDataDir + newDataDir ); + + const newRuntime = { + ...selectedNode.runtime, + dataDir: newDataDir, + }; + + const newConfig = { + ...selectedNode.config, + configValuesMap: { + ...updatedConfig, + dataDir: newDataDir, + }, + }; + + const newNode = { + ...selectedNode, + config: newConfig, + runtime: newRuntime, + }; + + setSelectedNode(newNode); + modalOnChangeConfig({ newDataDir, selectedNode }); } else { const newConfig = { ...selectedNode.config, configValuesMap: { - ...configValuesMap, + ...updatedConfig, [configKey]: newValue, }, }; console.log('updating node with newConfig: ', newConfig); - await electron.updateNode(selectedNode.id, { + const newNode = { + ...selectedNode, config: newConfig, + }; + setSelectedNode(newNode as Node); + modalOnChangeConfig({ + settingsConfig: newConfig, + selectedNode, }); } // todo: show the user it was successful or not @@ -161,43 +245,24 @@ const NodeSettingsWrapper = ({ } }; - const onClickRemoveNode = useCallback(() => { - setIsRemoveNodeModalOpen(true); - }, []); - - const onCloseRemoveNode = useCallback( - (action: RemoveNodeAction) => { - // if node was removed, close the node settings - // select another node? - // if remove node was "cancel"'d, keep settings open - console.log('NodeSettingsWrapper: onCloseRemoveNode'); - setIsRemoveNodeModalOpen(false); - if (action === 'remove') { - onClickClose(); - } - }, - [onClickClose] - ); - return ( - <> - - - + { + dispatch( + setModalState({ + isModalOpen: true, + screen: { route: 'removeNode', type: 'alert' }, + }) + ); + }} + nodeStartCommand={sNodeStartCommand} + /> ); }; diff --git a/src/renderer/Presentational/NodeSettingsModal/WalletSettings.css.ts b/src/renderer/Presentational/NodeSettings/WalletSettings.css.ts similarity index 100% rename from src/renderer/Presentational/NodeSettingsModal/WalletSettings.css.ts rename to src/renderer/Presentational/NodeSettings/WalletSettings.css.ts diff --git a/src/renderer/Presentational/NodeSettingsModal/WalletSettings.tsx b/src/renderer/Presentational/NodeSettings/WalletSettings.tsx similarity index 99% rename from src/renderer/Presentational/NodeSettingsModal/WalletSettings.tsx rename to src/renderer/Presentational/NodeSettings/WalletSettings.tsx index f6efd9756..0523ea2a7 100644 --- a/src/renderer/Presentational/NodeSettingsModal/WalletSettings.tsx +++ b/src/renderer/Presentational/NodeSettings/WalletSettings.tsx @@ -271,7 +271,7 @@ export const WalletSettings = ({
{networkDetails[key as keyof NetworkLabelsProps]}
+ {renderContent()} + + ); +}; +export default Notifications; diff --git a/src/renderer/Presentational/Notifications/NotificationsWrapper.tsx b/src/renderer/Presentational/Notifications/NotificationsWrapper.tsx new file mode 100644 index 000000000..5a1a06406 --- /dev/null +++ b/src/renderer/Presentational/Notifications/NotificationsWrapper.tsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { getNotifications, markAllAsRead } from '../../../main/notifications'; +import Notifications from './Notifications'; + +const NotificationsWrapper = () => { + const [notifications, setNotifications] = useState(getNotifications); + + const markAllNotificationsAsRead = () => { + const updatedNotifications = markAllAsRead(); + setNotifications(updatedNotifications); + }; + + const onSettingsClick = () => { + console.log('setting was clicked!'); + // removeNotifications(); + // setNotifications(getNotifications); + // addNotifications([ + // { + // unread: true, + // status: 'info', + // title: 'Scheduled for Sync Commitee Duty', + // description: 'Validator 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + // timestamp: 1673384953, + // }, + // { + // unread: true, + // status: 'info', + // title: 'Reward for slashing another validator', + // description: 'Validator 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + // timestamp: 1673384953, + // }, + // { + // unread: true, + // status: 'completed', + // title: 'Client successfuly updated', + // description: 'Lodestar consensus client', + // timestamp: 1673384953, + // }, + // { + // unread: true, + // status: 'download', + // title: 'Client update available', + // description: 'Lodestar consensus client', + // timestamp: 1673384953, + // }, + // { + // unread: false, + // status: 'warning', + // title: 'More than 40 log errors in one hour', + // description: 'Lodestar consensus client', + // timestamp: 1673384953, + // }, + // { + // unread: false, + // status: 'warning', + // title: 'Disk usage near 90%', + // description: + // 'All nodes affected. Consider upgrading your disk to one with at least 2TB of storage.', + // timestamp: 1673384953, + // }, + // { + // unread: false, + // status: 'warning', + // title: 'Internet connection down for 12 minutes', + // description: 'All nodes affected', + // timestamp: 1673384953, + // }, + // ]); + // setNotifications(getNotifications); + // setNotifications(removeNotifications); + // addNotification({ + // unread: true, + // status: 'info', + // title: 'Scheduled for Sync Commitee Duty', + // description: 'Validator 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + // timestamp: moment().unix(), + // }); + // setNotifications(getNotifications); + }; + + const onNotificationItemClick = () => { + console.log('notification was clicked, open the route'); + }; + + return ( + + ); +}; +export default NotificationsWrapper; diff --git a/src/renderer/Presentational/Notifications/notifications.css.ts b/src/renderer/Presentational/Notifications/notifications.css.ts new file mode 100644 index 000000000..a6cb513b2 --- /dev/null +++ b/src/renderer/Presentational/Notifications/notifications.css.ts @@ -0,0 +1,31 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '../../Generics/redesign/theme.css'; + +export const container = style({ + margin: '64px 40px', + boxSizing: 'border-box', +}); + +export const headerContainer = style({ + display: 'flex', + flexDirection: 'row', + marginBottom: 32, + gap: 5, + alignItems: 'center', +}); + +export const spacer = style({ + flexGrow: 1, +}); + +export const titleStyle = style({ + alignSelf: 'stretch', + fontStyle: 'normal', + fontWeight: '590', + fontSize: '32px', + lineHeight: '100%', + textTransform: 'capitalize', + color: vars.color.font, +}); + +export const emptyNotifications = style({}); diff --git a/src/renderer/Presentational/Notifications/notificationsData.json b/src/renderer/Presentational/Notifications/notificationsData.json new file mode 100644 index 000000000..bc2d071b2 --- /dev/null +++ b/src/renderer/Presentational/Notifications/notificationsData.json @@ -0,0 +1,51 @@ +[ + { + "unread": true, + "status": "info", + "title": "Scheduled for Sync Commitee Duty", + "description": "Validator 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "timestamp": 1673384953 + }, + { + "unread": true, + "status": "info", + "title": "Reward for slashing another validator", + "description": "Validator 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "timestamp": 1673384953 + }, + { + "unread": true, + "status": "completed", + "title": "Client successfuly updated", + "description": "Lodestar consensus client", + "timestamp": 1673384953 + }, + { + "unread": true, + "status": "download", + "title": "Client update available", + "description": "Lodestar consensus client", + "timestamp": 1673384953 + }, + { + "unread": false, + "status": "warning", + "title": "More than 40 log errors in one hour", + "description": "Lodestar consensus client", + "timestamp": 1673384953 + }, + { + "unread": false, + "status": "warning", + "title": "Disk usage near 90%", + "description": "All nodes affected. Consider upgrading your disk to one with at least 2TB of storage.", + "timestamp": 1673384953 + }, + { + "unread": false, + "status": "warning", + "title": "Internet connection down for 12 minutes", + "description": "All nodes affected", + "timestamp": 1673384953 + } +] diff --git a/src/renderer/Presentational/PreferencesModal/Preferences.tsx b/src/renderer/Presentational/Preferences/Preferences.tsx similarity index 91% rename from src/renderer/Presentational/PreferencesModal/Preferences.tsx rename to src/renderer/Presentational/Preferences/Preferences.tsx index 0d348a59e..b612cb250 100644 --- a/src/renderer/Presentational/PreferencesModal/Preferences.tsx +++ b/src/renderer/Presentational/Preferences/Preferences.tsx @@ -1,10 +1,13 @@ import { useTranslation } from 'react-i18next'; - -import { Modal } from '../../Generics/redesign/Modal/Modal'; import LineLabelSettings from '../../Generics/redesign/LabelSetting/LabelSettings'; import { Toggle } from '../../Generics/redesign/Toggle/Toggle'; import LanguageSelect from '../../LanguageSelect'; -import { captionText, selectedThemeImage, themeImage } from './preferences.css'; +import { + preferencesContainer, + captionText, + selectedThemeImage, + themeImage, +} from './preferences.css'; import DarkModeThumbnail from '../../assets/images/artwork/DarkModeThumbnail.png'; import LightModeThumbnail from '../../assets/images/artwork/LightModeThumbnail.png'; import AutoDarkLightModeThumbnail from '../../assets/images/artwork/AutoDarkLightModeThumbnail.png'; @@ -14,8 +17,6 @@ import { HorizontalLine } from '../../Generics/redesign/HorizontalLine/Horizonta export type ThemeSetting = 'light' | 'dark' | 'auto'; export type Preference = 'theme' | 'isOpenOnStartup'; export interface PreferencesProps { - isOpen: boolean; - onClose: () => void; themeSetting?: ThemeSetting; isOpenOnStartup?: boolean; version?: string; @@ -23,8 +24,6 @@ export interface PreferencesProps { } const Preferences = ({ - isOpen, - onClose, themeSetting, isOpenOnStartup, version, @@ -39,11 +38,7 @@ const Preferences = ({ }; return ( - +
{t('Appearance')}
{[ @@ -124,7 +120,7 @@ const Preferences = ({ ]} /> NiceNode version {version} - +
); }; diff --git a/src/renderer/Presentational/PreferencesModal/PreferencesWrapper.tsx b/src/renderer/Presentational/Preferences/PreferencesWrapper.tsx similarity index 82% rename from src/renderer/Presentational/PreferencesModal/PreferencesWrapper.tsx rename to src/renderer/Presentational/Preferences/PreferencesWrapper.tsx index 86c6cd91f..4af747f73 100644 --- a/src/renderer/Presentational/PreferencesModal/PreferencesWrapper.tsx +++ b/src/renderer/Presentational/Preferences/PreferencesWrapper.tsx @@ -1,14 +1,16 @@ import { useCallback, useEffect, useState } from 'react'; +import { ModalConfig } from '../ModalManager/modalUtils'; import electron from '../../electronGlobal'; import Preferences, { Preference, ThemeSetting } from './Preferences'; export interface PreferencesWrapperProps { - isOpen: boolean; - onClose: () => void; + modalOnChangeConfig: (config: ModalConfig, save?: boolean) => void; } -const PreferencesWrapper = ({ isOpen, onClose }: PreferencesWrapperProps) => { +const PreferencesWrapper = ({ + modalOnChangeConfig, +}: PreferencesWrapperProps) => { const [sThemeSetting, setThemeSetting] = useState(); const [sIsOpenOnStartup, setIsOpenOnStartup] = useState(); const [sNiceNodeVersion, setNiceNodeVersion] = useState(); @@ -43,20 +45,22 @@ const PreferencesWrapper = ({ isOpen, onClose }: PreferencesWrapperProps) => { if (preference === 'theme') { const theme = value as ThemeSetting; setThemeSetting(theme); - await electron.setThemeSetting(theme); + modalOnChangeConfig({ + theme, + }); } else if (preference === 'isOpenOnStartup') { const isOpenOnStartup = value as boolean; setIsOpenOnStartup(isOpenOnStartup); - await electron.setIsOpenOnStartup(isOpenOnStartup); + modalOnChangeConfig({ + isOpenOnStartup, + }); } }, - [] + [modalOnChangeConfig] ); return ( void; - nodeDisplayName?: string; nodeStorageUsedGBs?: number; - onClickRemoveNode: (isDeletingData: boolean) => void; - errorMessage?: string; + modalOnChangeConfig: (config: ModalConfig, save?: boolean) => void; + selectedNode?: Node; } const RemoveNode = ({ - isOpen, - onClickClose, - nodeDisplayName, nodeStorageUsedGBs, - onClickRemoveNode, - errorMessage, + modalOnChangeConfig, + selectedNode, }: RemoveNodeProps) => { - const { t: tNiceNode } = useTranslation(); - const [sShouldDeleteNodeStorage, setShouldDeleteNodeStorage] = - useState(false); const [sNodeStorageMessage, setNodeStorageMessage] = useState( 'calculating data size...' ); @@ -42,36 +29,22 @@ const RemoveNode = ({ } }, [nodeStorageUsedGBs]); - const onClickRemove = useCallback(() => { - onClickRemoveNode(sShouldDeleteNodeStorage); - }, [onClickRemoveNode, sShouldDeleteNodeStorage]); - return ( - -
- - Are you sure you want to remove {nodeDisplayName}? - - { - console.log('setShouldDeleteNodeStorage: ', isChecked); - setShouldDeleteNodeStorage(isChecked); - }} - /> - {errorMessage && ( - - )} -
-
-
-
+
+

+ All settings and data for this node will be removed from your computer. +

+

+ Optionally you can choose to keep the current chain data to shorten + future sync times for NiceNode or alternative uses. +

+ { + modalOnChangeConfig({ isDeleteStorage: !isChecked, selectedNode }); + }} + /> +
); }; diff --git a/src/renderer/Presentational/RemoveNodeModal/RemoveNodeWrapper.tsx b/src/renderer/Presentational/RemoveNodeModal/RemoveNodeWrapper.tsx index ee2a4d167..29d88a3cc 100644 --- a/src/renderer/Presentational/RemoveNodeModal/RemoveNodeWrapper.tsx +++ b/src/renderer/Presentational/RemoveNodeModal/RemoveNodeWrapper.tsx @@ -1,20 +1,16 @@ -import { useCallback, useEffect, useState } from 'react'; -import electron from '../../electronGlobal'; +import { useEffect, useState } from 'react'; +import { ModalConfig } from '../ModalManager/modalUtils'; import { useAppSelector } from '../../state/hooks'; import { selectSelectedNode } from '../../state/node'; import RemoveNode from './RemoveNode'; -export type RemoveNodeAction = 'cancel' | 'remove'; - export interface RemoveNodeWrapperProps { - isOpen: boolean; - onClose: (action: RemoveNodeAction) => void; + modalOnChangeConfig: (config: ModalConfig, save?: boolean) => void; } -const RemoveNodeWrapper = ({ isOpen, onClose }: RemoveNodeWrapperProps) => { +const RemoveNodeWrapper = ({ modalOnChangeConfig }: RemoveNodeWrapperProps) => { const selectedNode = useAppSelector(selectSelectedNode); const [sNodeStorageUsedGBs, setNodeStorageUsedGBs] = useState(); - const [sError, setError] = useState(''); useEffect(() => { console.log(selectedNode); @@ -24,42 +20,15 @@ const RemoveNodeWrapper = ({ isOpen, onClose }: RemoveNodeWrapperProps) => { } else { setNodeStorageUsedGBs(undefined); } + modalOnChangeConfig({ selectedNode }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedNode]); - const onClickRemoveNode = useCallback( - async (isDeletingData: boolean) => { - // open remove node modal/prompt - console.log('onClickRemoveNode', selectedNode?.id, isDeletingData); - if (!selectedNode) { - throw new Error( - 'Unable to remove the node. No selected node detected.' - ); - } - try { - setError(''); - await electron.removeNode(selectedNode.id, { - isDeleteStorage: isDeletingData, - }); - // unselect the current node? - // or call remove node through a hook which updates this? - onClose('remove'); - } catch (err) { - console.error(err); - setError( - 'There was an error removing the node. Try again and please report the error to the NiceNode team in Discord.' - ); - } - }, - [selectedNode, onClose] - ); - return ( onClose('cancel')} - onClickRemoveNode={onClickRemoveNode} - errorMessage={sError} + modalOnChangeConfig={modalOnChangeConfig} + selectedNode={selectedNode} /> ); }; diff --git a/src/renderer/Presentational/RemoveNodeModal/removeNode.css.ts b/src/renderer/Presentational/RemoveNodeModal/removeNode.css.ts new file mode 100644 index 000000000..2b5d2d725 --- /dev/null +++ b/src/renderer/Presentational/RemoveNodeModal/removeNode.css.ts @@ -0,0 +1,12 @@ +import { style } from '@vanilla-extract/css'; + +export const container = style({ + display: 'flex', + flexDirection: 'column', + gap: 16, + width: 332, +}); + +export const removeText = style({ + lineHeight: '18px', +}); diff --git a/src/renderer/Presentational/Sidebar/Sidebar.tsx b/src/renderer/Presentational/Sidebar/Sidebar.tsx index e88bb3dba..75b38ddc5 100644 --- a/src/renderer/Presentational/Sidebar/Sidebar.tsx +++ b/src/renderer/Presentational/Sidebar/Sidebar.tsx @@ -1,4 +1,6 @@ -import { useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { setModalState } from '../../state/modal'; +import { getNotifications } from '../../../main/notifications'; import { useAppDispatch } from '../../state/hooks'; import { updateSelectedNodeId } from '../../state/node'; import { NodeId, NodeStatus, UserNodes } from '../../../common/node'; @@ -12,9 +14,6 @@ import { SidebarTitleItem } from '../../Generics/redesign/SidebarTitleItem/Sideb import { container, nodeList, itemList, titleItem } from './sidebar.css'; import { IconId } from '../../assets/images/icons'; // import { NodeIconId } from '../../assets/images/nodeIcons'; -import { Modal } from '../../Generics/redesign/Modal/Modal'; -import AddNodeStepper from '../AddNodeStepper/AddNodeStepper'; -import PreferencesWrapper from '../PreferencesModal/PreferencesWrapper'; import { DockerStoppedBanner } from '../DockerInstallation/StartDockerBanner'; export interface SidebarProps { @@ -35,6 +34,11 @@ export interface SidebarProps { } const itemListData: { iconId: IconId; label: string; count?: number }[] = [ + { + iconId: 'bell', + label: 'Notifications', + count: getNotifications().length, // this needs to be updated based on changes in global state + }, { iconId: 'add', label: 'Add Node', @@ -76,9 +80,6 @@ const Sidebar = ({ selectedNodeId, }: SidebarProps) => { const dispatch = useAppDispatch(); - const [sIsModalOpenAddNode, setIsModalOpenAddNode] = useState(); - const [sIsModalOpenSettings, setIsModalOpenSettings] = - useState(false); // const nodeListObject = { nodeService: [], validator: [], singleClients: [] }; // sUserNodes?.nodeIds.forEach((nodeId: NodeId) => { @@ -114,16 +115,7 @@ const Sidebar = ({ return null; }); }; - - const onClickLinkItem = useCallback((linkItemId: string) => { - console.log('sidebar link item clicked: ', linkItemId); - if (linkItemId === 'add') { - // open add node dialog - setIsModalOpenAddNode(true); - } else if (linkItemId === 'preferences') { - setIsModalOpenSettings(true); - } - }, []); + const navigate = useNavigate(); return (
@@ -159,7 +151,10 @@ const Sidebar = ({ info={spec.displayName} status={sidebarStatus} selected={selectedNodeId === node.id} - onClick={() => dispatch(updateSelectedNodeId(node.id))} + onClick={() => { + navigate('/main/node'); + dispatch(updateSelectedNodeId(node.id)); + }} /> ); })} @@ -172,30 +167,32 @@ const Sidebar = ({ iconId={item.iconId} label={item.label} count={item.count} - onClick={() => onClickLinkItem(item.iconId)} + onClick={() => { + console.log('sidebar link item clicked: ', item.iconId); + if (item.iconId === 'add') { + dispatch( + setModalState({ + isModalOpen: true, + screen: { route: 'addNode', type: 'modal' }, + }) + ); + } else if (item.iconId === 'preferences') { + dispatch( + setModalState({ + isModalOpen: true, + screen: { route: 'preferences', type: 'modal' }, + }) + ); + } else if (item.iconId === 'bell') { + navigate('/main/notification'); + } else if (item.iconId === 'health') { + navigate('/main/system'); + } + }} /> ); })}
- setIsModalOpenAddNode(false)} - isFullScreen - > - { - console.log(newValue); - if (newValue === 'done' || newValue === 'cancel') { - setIsModalOpenAddNode(false); - } - }} - /> - - setIsModalOpenSettings(false)} - />
); }; diff --git a/src/renderer/Presentational/SystemMonitor/SystemMonitor.tsx b/src/renderer/Presentational/SystemMonitor/SystemMonitor.tsx new file mode 100644 index 000000000..1031108f0 --- /dev/null +++ b/src/renderer/Presentational/SystemMonitor/SystemMonitor.tsx @@ -0,0 +1,14 @@ +import { SystemMonitor as SystemMonitorLabels } from '../../Generics/redesign/SystemMonitor/SystemMonitor'; +import { headerContainer, titleStyle } from './systemMonitor.css'; + +const SystemMonitor = () => { + return ( + <> +
+
System monitor
+
+ + + ); +}; +export default SystemMonitor; diff --git a/src/renderer/Presentational/SystemMonitor/systemMonitor.css.ts b/src/renderer/Presentational/SystemMonitor/systemMonitor.css.ts new file mode 100644 index 000000000..7575f8441 --- /dev/null +++ b/src/renderer/Presentational/SystemMonitor/systemMonitor.css.ts @@ -0,0 +1,22 @@ +import { style } from '@vanilla-extract/css'; +import { vars } from '../../Generics/redesign/theme.css'; + +export const headerContainer = style({ + display: 'flex', + flexDirection: 'row', + marginBottom: 32, + gap: 5, + alignItems: 'center', +}); + +export const titleStyle = style({ + alignSelf: 'stretch', + fontStyle: 'normal', + fontWeight: '590', + fontSize: '32px', + lineHeight: '100%', + textTransform: 'capitalize', + color: vars.color.font, +}); + +export const emptyNotifications = style({}); diff --git a/src/renderer/__mocks__/custom-preload-mock.ts b/src/renderer/__mocks__/custom-preload-mock.ts index b080fad62..995fdac75 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 getNodeStartCommand = () => {}; export const stopNode = () => {}; +export const updateNodeDataDir = () => {}; export const openDialogForNodeDataDir = () => {}; export const openDialogForStorageLocation = () => {}; export const getNodesDefaultStorageLocation = () => '/user/storage/nodes/'; diff --git a/src/renderer/app.css.ts b/src/renderer/app.css.ts index b0247e9c2..63adb18a9 100644 --- a/src/renderer/app.css.ts +++ b/src/renderer/app.css.ts @@ -1,17 +1,26 @@ import { style, ComplexStyleRule } from '@vanilla-extract/css'; -import { vars } from './Generics/redesign/theme.css'; export const dragWindowContainer = style({ WebkitUserSelect: 'none', '-webkit-app-region': 'drag', - height: 30, + height: 52, width: '100vw', position: 'absolute', top: 0, - zIndex: 100, + zIndex: 3, cursor: 'grab', - ':hover': { - background: vars.color.background92, - opacity: 0.3, - }, } as ComplexStyleRule); // fix for lacking '-webkit-app-region' type + +export const homeContainer = style({ + display: 'flex', + flexDirection: 'row', + width: '100%', + height: '100%', +}); + +export const contentContainer = style({ + padding: '64px 40px', + boxSizing: 'border-box', + flex: 1, + overflow: 'auto', +}); diff --git a/src/renderer/index.ejs b/src/renderer/index.ejs index 10d7af622..1b04ee2fa 100644 --- a/src/renderer/index.ejs +++ b/src/renderer/index.ejs @@ -6,6 +6,7 @@ http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline'" /> + NiceNode diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index 5d3188193..26d7079eb 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -57,7 +57,8 @@ declare global { startNode(nodeId: NodeId): void; getNodeStartCommand(nodeId: NodeId): string; stopNode(nodeId: NodeId): void; - openDialogForNodeDataDir(nodeId: NodeId): void; + updateNodeDataDir(node: Node, newDataDir: string): void; + openDialogForNodeDataDir(nodeId: NodeId): string; openDialogForStorageLocation(): CheckStorageDetails; updateNodeUsedDiskSpace(nodeId: NodeId): void; deleteNodeStorage(nodeId: NodeId): boolean; diff --git a/src/renderer/state/modal.ts b/src/renderer/state/modal.ts new file mode 100644 index 000000000..6941da0ad --- /dev/null +++ b/src/renderer/state/modal.ts @@ -0,0 +1,43 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from './store'; + +export interface ModalScreen { + route: string | undefined; + type: 'alert' | 'modal' | undefined; +} + +// Define a type for the slice state +export interface ModalState { + isModalOpen: boolean; + screen: ModalScreen; +} + +// Define the initial state using that type +export const initialState: ModalState = { + isModalOpen: false, + screen: { + route: undefined, + type: undefined, + }, +}; + +console.log('Intial modal state: ', initialState); + +export const modalSlice = createSlice({ + name: 'modal', + // `createSlice` will infer the state type from the `initialState` argument + initialState, + reducers: { + setModalState: (state, action: PayloadAction) => { + const { isModalOpen, screen } = action.payload; + state.isModalOpen = isModalOpen; + state.screen = screen; + }, + }, +}); + +export const { setModalState } = modalSlice.actions; + +export const getModalState = (state: RootState): ModalState => state.modal; + +export default modalSlice.reducer; diff --git a/src/renderer/state/store.ts b/src/renderer/state/store.ts index e5a1573b8..2c922a5bd 100644 --- a/src/renderer/state/store.ts +++ b/src/renderer/state/store.ts @@ -5,10 +5,12 @@ import { RtkqNodeService } from './nodeService'; import { RtkqSettingsService } from './settingsService'; import { RtkqNetwork } from './network'; import nodeReducer from './node'; +import modalReducer from './modal'; export const store = configureStore({ reducer: { node: nodeReducer, + modal: modalReducer, [RtkqNodeService.reducerPath]: RtkqNodeService.reducer, [RtkqSettingsService.reducerPath]: RtkqSettingsService.reducer, [RtkqExecutionWs.reducerPath]: RtkqExecutionWs.reducer, diff --git a/src/stories/Generic/Button.stories.tsx b/src/stories/Generic/Button.stories.tsx index 0256ad95d..2a74cfae0 100644 --- a/src/stories/Generic/Button.stories.tsx +++ b/src/stories/Generic/Button.stories.tsx @@ -14,13 +14,13 @@ const Template: ComponentStory = (args) =>