Skip to content

Commit

Permalink
feat: Notifications view, routing, system monitor view, and node setu…
Browse files Browse the repository at this point in the history
…p flow (#172) and Modal manager (#175)

* added notification icon

* added shadow and checked

* added notification item

* notifications view

* added notifications wrapper to handle data

* check if notifications are read, change button state

* localStorage notifications working

* added various notification methods

* disabled notification api for now

* add route files, reset package.json to start fresh

* RN modules now working, but issue parsing TS in JS files in RN deps

* revert all changes

* react-router-dom working

* fixed containers, buttons, prepping for splash

* fixed button

* clean, added step

* added no node view, moved NodeScreen

* added NodeSetup route, modified modal for initial and during app

* removed displayNotification for now

* moved logs into route

* feat: Modal manager (#175)

* modalManager working using context

* added modal state using react-redux

* moved modals into modalManager

* additional adjustments on tabs, to support new modal

* modal save preferences working

* decouple nodeDir window and saving new nodeDir, start work on modal config manager

* moved view related logic into modal for easier testing

* add disableButton

* node stepper finally working

* added alert style, converted removenode

* disable done button until docker is complete

* AddEthereumNode now works for both modal and initial setup

* fixed node select and remove node display

* improved initial render

* capture all settings on initial render so they can be saved when button is pressed

* now only adds keys if not found, wont overwrite

* fixes onChange input behavior on initial render of the screen

* moved backend calls into modal components

* moved common const, type, func into modalUtils

* added min height/width to electron, moved modal files

* modified modal layouts

* adjusted modal, added scroll bar dark/light mode

* added node reqs to add node modal flow

* differentiate modalOnClose and modalOnCancel

---------

Co-authored-by: Johns Gresham <[email protected]>
  • Loading branch information
corn-potage and jgresham authored Mar 6, 2023
1 parent 2aa3e5d commit 84e8358
Show file tree
Hide file tree
Showing 98 changed files with 2,728 additions and 1,018 deletions.
24 changes: 24 additions & 0 deletions src/main/consts/notifications.ts
Original file line number Diff line number Diff line change
@@ -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',
},
},
});
17 changes: 11 additions & 6 deletions src/main/dialog.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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<
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import installDocker from './docker/install';
import {
openDialogForNodeDataDir,
openDialogForStorageLocation,
updateNodeDataDir,
} from './dialog';
import { getNodeLibrary } from './state/nodeLibrary';
import {
Expand Down Expand Up @@ -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);
});
Expand Down
4 changes: 4 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
57 changes: 57 additions & 0 deletions src/main/notifications.ts
Original file line number Diff line number Diff line change
@@ -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');
};
2 changes: 2 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () =>
Expand Down
165 changes: 89 additions & 76 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className={dragWindowContainer} />
{children}
</>
);
};

const Main = () => {
return (
<WindowContainer>
<div className={homeContainer}>
<SidebarWrapper />
<Outlet />
<DataRefresher />
</div>
</WindowContainer>
);
};

const System = () => {
return <SystemMonitor />;
};

export default function App() {
const dispatch = useAppDispatch();
const [sHasSeenSplashscreen, setHasSeenSplashscreen] = useState<boolean>();
const [sHasClickedGetStarted, setHasClickedGetStarted] = useState<boolean>();
const [sIsModalOpenAddNode, setIsModalOpenAddNode] = useState<boolean>();

// const isStartOnLogin = await electron.getStoreValue('isStartOnLogin');
// console.log('isStartOnLogin: ', isStartOnLogin);
// setIsOpenOnLogin(isStartOnLogin);

useEffect(() => {
const callAsync = async () => {
Expand All @@ -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 (
<ThemeManager>
{sHasSeenSplashscreen === false ? (
<>
{!sHasClickedGetStarted && (
<NNSplash onClickGetStarted={onClickSplashGetStarted} />
)}
</>
) : (
<>
<div className={dragWindowContainer} />
<div
style={{
display: 'flex',
flexDirection: 'row',
width: '100%',
height: '100%',
}}
>
<SidebarWrapper />
<div style={{ flex: 1, overflow: 'auto' }}>
<NodeScreen />
</div>
</div>

<DataRefresher />
{/* Todo: remove this when Modal Manager is created */}
<Modal
title=""
isOpen={sIsModalOpenAddNode}
onClickCloseButton={() => setIsModalOpenAddNode(false)}
isFullScreen
>
<AddNodeStepper
onChange={(newValue: 'done' | 'cancel') => {
console.log(newValue);
if (newValue === 'done' || newValue === 'cancel') {
setIsModalOpenAddNode(false);
}
}}
<MemoryRouter initialEntries={[initialPage]}>
<ModalManager />
<Routes>
<Route path="/">
<Route
index
path="/setup"
element={
<WindowContainer>
<NodeSetup />
</WindowContainer>
}
/>
</Modal>
</>
)}
<Route path="/main" element={<Main />}>
<Route
path="/main/node"
element={
<div className={contentContainer}>
<NodeScreen />
</div>
}
/>
<Route path="/main/node/logs" element={<LogsWrapper />} />
<Route
path="/main/notification"
element={
<div className={contentContainer}>
<NotificationsWrapper />
</div>
}
/>
<Route
path="/main/system"
element={
<div className={contentContainer}>
<System />
</div>
}
/>
</Route>
{/* Using path="*"" means "match anything", so this route
acts like a catch-all for URLs that we don't have explicit
routes for. */}
{/* <Route path="*" element={<NoMatch />} /> */}
</Route>
</Routes>
</MemoryRouter>
</ThemeManager>
);
};

export default function App() {
return (
<Router>
<Routes>
<Route path="/" element={<MainScreen />} />
</Routes>
</Router>
);
}
Loading

0 comments on commit 84e8358

Please sign in to comment.