├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── .python-version ├── LICENSE ├── README.md ├── app ├── WindowManager.js ├── mist-integration.js ├── shell-electron.js ├── shell-tau.js ├── store │ ├── InitialState.js │ └── SyncedReduxStore.js └── utils │ └── is.js ├── docs └── CONTRIBUTING.md ├── i18n ├── app.en.i18n.json └── mist.en.i18n.json ├── icons ├── icon.icns ├── icon.ico ├── icon.png └── icon2x.png ├── package.json ├── preload ├── preload-webview.js └── preload.js ├── public ├── errors │ ├── 400.html │ ├── 404.html │ └── 500.html ├── examples │ ├── index.html │ └── request-access.html ├── favicon.ico ├── index.html └── manifest.json ├── settings.dev.json ├── src ├── API │ ├── Grid.js │ ├── Helpers.js │ ├── Ipc.js │ ├── LocalStore.js │ ├── Mist.js │ ├── ReduxStoreSynced.js │ ├── Web3.js │ ├── i18n.js │ └── index.js ├── components │ ├── App.js │ ├── Apps │ │ ├── AppItem.js │ │ └── index.js │ ├── GenericErrorBoundary.js │ ├── GenericProvider.js │ ├── NavTabs.js │ ├── Plugins │ │ ├── Metadata │ │ │ └── index.js │ │ ├── NodeInfo │ │ │ ├── NodeInfoBox.js │ │ │ ├── NodeInfoDot.js │ │ │ └── index.js │ │ ├── PluginConfig │ │ │ ├── AboutPlugin.js │ │ │ ├── DependencyCard.js │ │ │ ├── DynamicConfigForm │ │ │ │ ├── FlagPreview.js │ │ │ │ ├── FormItem.js │ │ │ │ └── index.js │ │ │ ├── VersionList │ │ │ │ ├── LatestVersionWarning.js │ │ │ │ ├── VersionListItem.js │ │ │ │ ├── VersionsAvailableText.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── PluginsNav.js │ │ ├── PluginsNavListItem.js │ │ ├── Terminal │ │ │ ├── TerminalInput.js │ │ │ └── index.js │ │ └── index.js │ ├── Webview │ │ ├── UrlBar.jsx │ │ └── index.jsx │ ├── Window.js │ └── shared │ │ ├── Button.js │ │ ├── HelpFab.js │ │ ├── Notification.js │ │ ├── Select.js │ │ └── Spinner.js ├── icons │ ├── browse-icon@2x.png │ ├── icon.png │ └── icon2x.png ├── index.css ├── index.js ├── lib │ ├── flags.js │ └── utils.js ├── scrollbar-fix.css ├── store │ ├── index.js │ ├── middleware.js │ ├── plugin │ │ ├── actions.js │ │ ├── clientService.js │ │ ├── pluginService.js │ │ ├── reducer.js │ │ └── reducer.test.js │ └── rootReducer.js ├── test │ └── settings.test.js └── theme.js ├── tasks.js ├── webpack.config.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | version: 2 3 | jobs: 4 | build: 5 | docker: 6 | - image: circleci/node:10.5 7 | 8 | steps: 9 | - checkout 10 | 11 | - run: 12 | name: "Installing dependencies" 13 | command: yarn 14 | 15 | - run: yarn lint 16 | 17 | - run: yarn test --verbose --coverage 18 | 19 | - run: 20 | name: "Building and deploying" 21 | command: | 22 | if [[ "${CIRCLE_BRANCH}" == 'master' ]] || [[ "${CIRCLE_BRANCH}" == 'dev' ]]; then 23 | CI=false yarn deploy $CIRCLE_BRANCH; 24 | else 25 | CI=false yarn package alpha; 26 | fi; 27 | 28 | - store_artifacts: 29 | path: build/grid-ui*.zip 30 | 31 | - store_artifacts: 32 | path: build/manifest.json 33 | 34 | 35 | workflows: 36 | version: 2 37 | commit: 38 | jobs: 39 | - build 40 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | __mocks__ 2 | __tests__ 3 | dist 4 | node_modules 5 | src/i18n 6 | src/lib 7 | src/styles 8 | src/fakeAPI.js 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | browser: true, 5 | jasmine: true 6 | }, 7 | plugins: ['react'], 8 | parser: 'babel-eslint', 9 | parserOptions: { 10 | ecmaFeatures: { 11 | jsx: true 12 | } 13 | }, 14 | extends: ['airbnb', 'plugin:react/recommended'], 15 | rules: { 16 | 'arrow-body-style': 0, 17 | 'arrow-parens': 0, 18 | 'class-methods-use-this': 0, 19 | 'comma-dangle': 0, 20 | 'implicit-arrow-linebreak': 0, 21 | 'import/no-extraneous-dependencies': [ 22 | 2, 23 | { 24 | devDependencies: ['.storybook/**', 'src/stories/**'] 25 | } 26 | ], 27 | 'jsx-a11y/label-has-associated-control': 0, 28 | 'jsx-a11y/label-has-for': 0, 29 | 'function-paren-newline': 0, 30 | 'no-alert': 0, 31 | 'no-console': 0, 32 | 'no-underscore-dangle': 0, 33 | 'object-curly-newline': 0, 34 | 'operator-linebreak': 0, 35 | 'linebreak-style': 0, 36 | 'no-bitwise': 0, 37 | 'no-mixed-operators': 0, 38 | 'react/button-has-type': 0, 39 | 'react/display-name': 2, 40 | 'react/forbid-prop-types': 0, 41 | 'react/jsx-closing-bracket-location': 0, 42 | 'react/jsx-one-expression-per-line': 0, 43 | 'react/no-array-index-key': 0, 44 | 'react/prefer-stateless-function': 0, 45 | 'react/prop-types': 2, 46 | 'react/require-default-props': 0, 47 | 'react/jsx-filename-extension': 0, 48 | 'react/jsx-wrap-multilines': 0, 49 | indent: 0, 50 | semi: 0 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | #### What does it do? 2 | 3 | #### Any helpful background information? 4 | 5 | #### New dependencies? What are they used for? 6 | 7 | #### Relevant screenshots? 8 | 9 | #### Does it close any issues? 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | node_modules 6 | 7 | /dist 8 | dist 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | package-lock.json 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: false 3 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 2.7.15 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## :no_entry: Deprecated :no_entry: 2 | This project is not supported anymore. 3 | 4 |

5 | 6 |

7 | 8 |

9 | Build Status 10 |

11 | 12 | # Ethereum Grid 13 | 14 | Grid is a desktop application that allows you to securely download, configure and use various clients and tools in the Ethereum ecosystem. Download the [latest version](https://grid.ethereum.org/). 15 | 16 | ![](https://imgur.com/T3Tt65P.jpg) 17 | 18 | See this [introductory post](https://medium.com/ethereum-grid/introducing-ethereum-grid-1e65e7fb771e) to learn more about the motivations behind the project. Release announcements and tutorials are released on the project [Medium publication](https://medium.com/ethereum-grid). 19 | 20 | ## Development 21 | 22 | This repo is the web application hosted by [Grid](https://github.com/ethereum/grid). 23 | 24 | ### Quick Start 25 | 26 | Clone, install dependencies, and start the application: 27 | 28 | ``` 29 | git clone https://github.com/ethereum/grid-ui.git 30 | cd grid-ui 31 | yarn && yarn start 32 | ``` 33 | 34 | This will serve the application at `localhost:3080`, but little can be done without the [Grid](https://github.com/ethereum/grid) electron wrapper: 35 | 36 | ``` 37 | git clone https://github.com/ethereum/grid.git 38 | cd grid 39 | yarn && yarn start:dev 40 | ``` 41 | 42 | ### Contributing 43 | 44 | There are many ways to get involved with this project. Get started [here](/docs/CONTRIBUTING.md). 45 | -------------------------------------------------------------------------------- /app/WindowManager.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const is = require('./utils/is') 3 | const url = require('url') 4 | const { BrowserWindow } = require('electron') 5 | 6 | const devSettings = { 7 | openDevTools: (category) => { 8 | let openFor = ['renderer', 'popup'] 9 | //return openFor.includes(category) 10 | return true 11 | } 12 | } 13 | let forceDevTools = true 14 | 15 | let win; 16 | 17 | /* 18 | BrowserWindow.prototype.loadPackage = (asarPath) => { 19 | let file = path.join(asarPath, 'index.html') // locate entry point in package 20 | console.log('load file:', file) 21 | this.loadFile(file) 22 | } 23 | 24 | export default class WindowManager { 25 | createWindow() { 26 | // Create the browser window. 27 | win = new BrowserWindow({ 28 | width: 800, 29 | height: 600, 30 | webPreferences: { 31 | nodeIntegration: true, 32 | webviewTag: true, // mist specific: defaults to nodeIntegration value 33 | preload: path.join(__dirname, 'preload.js') 34 | } 35 | }); 36 | 37 | // Open the DevTools. 38 | win.webContents.openDevTools(); 39 | 40 | // Emitted when the window is closed. 41 | win.on('closed', () => { 42 | // Dereference the window object, usually you would store windows 43 | // in an array if your app supports multi windows, this is the time 44 | // when you should delete the corresponding element. 45 | win = null; 46 | }); 47 | 48 | return win; 49 | } 50 | loadPackage(asarPath){ 51 | let file = path.join(asarPath, 'index.html') // locate entry point in package 52 | win.loadFile(file) 53 | } 54 | } 55 | */ 56 | 57 | const PORT = process.env.PORT || 3000 58 | 59 | let isDev = false // FIXME investigate is.dev() 60 | let preloadPath = isDev ? path.join(__dirname, 'preload', 'preload.js') : path.join(__dirname, 'preload.js') 61 | // TODO if path does not exist process.exit() 62 | 63 | class WindowManager { 64 | 65 | createWindow (asarPath) { 66 | // Create the browser window. 67 | win = new BrowserWindow({ 68 | width: 1100, 69 | height: 720, 70 | webPreferences: { 71 | preload: preloadPath 72 | } 73 | }) 74 | 75 | // Open the DevTools. 76 | if((forceDevTools || is.dev()) && devSettings.openDevTools('renderer')){ 77 | win.webContents.openDevTools() 78 | } 79 | 80 | // Emitted when the window is closed. 81 | win.on('closed', () => { 82 | // Dereference the window object, usually you would store windows 83 | // in an array if your app supports multi windows, this is the time 84 | // when you should delete the corresponding element. 85 | win = null 86 | }) 87 | 88 | return win 89 | } 90 | showPopup(name, args) { 91 | let options = { 92 | width: 800, 93 | height: 400 94 | } 95 | let windowOptions = {} 96 | if (name === 'ClientUpdateAvailable') { 97 | windowOptions = { 98 | width: 600, 99 | height: 340, 100 | alwaysOnTop: false, 101 | resizable: false, 102 | maximizable: false 103 | } 104 | } 105 | if (name === 'RequestAccount' || name === 'CreateAccount') { 106 | windowOptions = { 107 | width: 450, 108 | height: 250, 109 | alwaysOnTop: true 110 | } 111 | } 112 | if (name === 'ConnectAccount') { 113 | windowOptions = { 114 | width: 460, 115 | height: 520, 116 | maximizable: false, 117 | minimizable: false, 118 | alwaysOnTop: true 119 | } 120 | } 121 | if (name === 'SendTransactionConfirmation') { 122 | windowOptions = { 123 | width: 580, 124 | height: 550, 125 | alwaysOnTop: true, 126 | enableLargerThanScreen: false, 127 | resizable: true 128 | } 129 | } 130 | if (name === 'SendTx') { 131 | windowOptions = { 132 | width: 580, 133 | height: 550, 134 | alwaysOnTop: true, 135 | enableLargerThanScreen: false, 136 | resizable: true 137 | } 138 | } 139 | if (name === 'TxHistory') { 140 | windowOptions = { 141 | width: 580, 142 | height: 465, 143 | alwaysOnTop: true, 144 | enableLargerThanScreen: false, 145 | resizable: true 146 | } 147 | } 148 | 149 | let config = Object.assign(options, windowOptions, { 150 | parent: win, // The child window will always show on top of the top window. 151 | modal: true, 152 | webPreferences: { 153 | preload: preloadPath 154 | } 155 | }) 156 | 157 | let popup = new BrowserWindow(config) 158 | popup.args = args 159 | 160 | if (is.dev()) { 161 | const PORT = process.env.PORT 162 | if(PORT){ 163 | popup.loadURL(`http://localhost:${PORT}/index.html?app=popup&name=${name}`) 164 | } // else TODO show error message 165 | } else { 166 | let ui = url.format({ 167 | slashes: true, 168 | protocol: 'file:', 169 | pathname: path.resolve(__dirname, 'index.html'), 170 | query: { 171 | app: 'popup', 172 | name: name 173 | } 174 | }) 175 | popup.loadURL(ui) 176 | } 177 | 178 | if((forceDevTools || is.dev()) && devSettings.openDevTools('popup')){ 179 | popup.webContents.openDevTools({mode: 'detach'}) 180 | } 181 | 182 | popup.setMenu(null) 183 | } 184 | } 185 | 186 | const windowManager = new WindowManager() 187 | module.exports = windowManager -------------------------------------------------------------------------------- /app/mist-integration.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | //const updater = require('./updater') 3 | const {app, dialog, BrowserWindow, Menu, MenuItem} = require('electron') 4 | 5 | // Keep a global reference of the window object, if you don't, the window will 6 | // be closed automatically when the JavaScript object is garbage collected. 7 | let win 8 | 9 | const PORT = process.env.PORT || 3000 10 | console.log('start react ui with devserver on port: ', PORT) 11 | 12 | 13 | function createReactMenu(version){ 14 | let popupMenu = (name, label) => {return new MenuItem({ 15 | label: label || name, 16 | click: () => { 17 | windowManager.showPopup(name) 18 | } 19 | })} 20 | 21 | const testPopupSubMenu = new Menu() 22 | testPopupSubMenu.append(popupMenu('ClientUpdateAvailable')) 23 | testPopupSubMenu.append(popupMenu('ConnectAccount')) 24 | testPopupSubMenu.append(popupMenu('SendTransactionConfirmation')) 25 | testPopupSubMenu.append(popupMenu('SendTx')) 26 | testPopupSubMenu.append(popupMenu('TxHistory')) 27 | testPopupSubMenu.append(popupMenu('Settings')) 28 | 29 | let reactSubMenu = new Menu() 30 | reactSubMenu.append(new MenuItem({ 31 | label: 'Check Update', 32 | click: async () => { 33 | let update = await updater.checkUpdate() 34 | if (!update) { 35 | dialog.showMessageBox({title: 'No update', message: 'You are using the latest version'}) 36 | return 37 | } 38 | dialog.showMessageBox({ 39 | title: 'Checking for updates', 40 | message: ` 41 | React UI update found: v${update.version} 42 | Press "OK" to download in background 43 | ` 44 | }, async () => { 45 | let download = await updater.downloadUpdate(update) 46 | if (!download.error) { 47 | dialog.showMessageBox({title: 'Update downloaded', message: `Press OK to reload for update to version ${download.version}`}) 48 | let asarPath = download.filePath 49 | start(asarPath, download.version) 50 | } else { 51 | dialog.showMessageBox({title: 'Download failed', message: `Error ${download.error}`}) 52 | } 53 | }) 54 | } 55 | })) 56 | reactSubMenu.append(new MenuItem({ 57 | label: 'Reload -> update', 58 | enabled: false 59 | })) 60 | reactSubMenu.append(popupMenu('Rollback')) 61 | reactSubMenu.append(new MenuItem({ 62 | label: 'Popups', 63 | submenu: testPopupSubMenu 64 | })) 65 | reactSubMenu.append(popupMenu('Settings')) 66 | 67 | reactSubMenu.append(new MenuItem({ 68 | label: `v${version}` 69 | })) 70 | 71 | return reactSubMenu 72 | } 73 | 74 | function updateMenuVersion(version){ 75 | 76 | let menu = Menu.getApplicationMenu() 77 | if (menu) { 78 | let reactSubMenu = createReactMenu(version) 79 | let menuNew = new Menu() 80 | menu.items.forEach(m => { 81 | if(m.label === 'React UI') { 82 | return 83 | } 84 | menuNew.append(m) 85 | }) 86 | menuNew.append(new MenuItem({label: 'React UI', submenu: reactSubMenu})) 87 | Menu.setApplicationMenu(menuNew) 88 | } 89 | } 90 | 91 | function start(asarPath, version){ 92 | // update menu to display current version 93 | 94 | updateMenuVersion(version) 95 | 96 | if (win) { 97 | // TODO let window manager handle 98 | win.loadFile(path.join(asarPath, 'index.html')) 99 | } else { 100 | windowManager.createWindow(asarPath) 101 | } 102 | } 103 | 104 | function run(options) { 105 | let reactSubMenu = createReactMenu('0.0.0') 106 | // let menu = Menu.getApplicationMenu() 107 | const menu = new Menu() 108 | menu.append(new MenuItem({label: 'React UI', submenu: reactSubMenu})) 109 | Menu.setApplicationMenu(menu) 110 | 111 | /* 112 | if (updater.isReady) { 113 | start(updater.asarPath) 114 | } else { 115 | updater.once('app-ready', (asarPath, version) => { 116 | console.log('found asar file', asarPath, version) 117 | start(asarPath, version) 118 | }) 119 | } 120 | */ 121 | } 122 | 123 | module.exports = { 124 | setup: run, 125 | showPopup: windowManager.showPopup, 126 | createWindow: windowManager.createWindow 127 | } -------------------------------------------------------------------------------- /app/shell-electron.js: -------------------------------------------------------------------------------- 1 | const { app, ipcMain } = require('electron') 2 | const path = require('path') 3 | const url = require('url') 4 | const is = require('./utils/is') 5 | 6 | const windowManager = require('./WindowManager') 7 | 8 | const updater = global.updater || { 9 | on: () => {} 10 | } 11 | 12 | // This method will be called when Electron has finished 13 | // initialization and is ready to create browser windows. 14 | // Some APIs can only be used after this event occurs. 15 | app.on('ready', () => { 16 | let win = windowManager.createWindow() 17 | 18 | if (is.dev()) { 19 | const PORT = process.env.PORT 20 | if(PORT){ 21 | win.loadURL(`http://localhost:${PORT}`) 22 | } // else TODO show error message 23 | } else { 24 | //win.loadFile(path.join(__dirname, 'index.html')) 25 | win.loadURL(url.format({ 26 | slashes: true, 27 | protocol: 'file:', 28 | pathname: path.resolve(__dirname, 'index.html/') 29 | })) 30 | } 31 | setupIpc() 32 | }) 33 | 34 | 35 | function setupIpc(){ 36 | ipcMain.on('backendAction_showPopup', (event, args) => { 37 | windowManager.showPopup(args.name, args.args) 38 | }) 39 | } 40 | 41 | 42 | /* 43 | function start() { 44 | Updater.on('app-ready', (asarPath) => { 45 | console.log('found asar file', asarPath) 46 | createWindow(asarPath) 47 | }) 48 | Updater.start() 49 | } 50 | Updater.on('update-available', () => {}) 51 | Updater.on('update-ready', () => { 52 | dialog.showMessageBox({ 53 | title: 'update available', 54 | message: 'will now restart' 55 | }, () => { 56 | createWindow() 57 | }) 58 | }) 59 | */ 60 | 61 | // Quit when all windows are closed. 62 | app.on('window-all-closed', () => { 63 | // On macOS it is common for applications and their menu bar 64 | // to stay active until the user quits explicitly with Cmd + Q 65 | if (process.platform !== 'darwin') { 66 | app.quit() 67 | } 68 | }) 69 | 70 | /* 71 | app.on('activate', () => { 72 | // On macOS it's common to re-create a window in the app when the 73 | // dock icon is clicked and there are no other windows open. 74 | if (win === null) { 75 | createWindow() 76 | } 77 | }) 78 | */ 79 | 80 | // In this file you can include the rest of your app's specific main process 81 | // code. You can also put them in separate files and require them here. -------------------------------------------------------------------------------- /app/shell-tau.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const url = require('url') 3 | const path = require('path') 4 | const { app, ipc, BrowserWindow } = require('electron') 5 | 6 | console.log('start tau app ...') 7 | const PORT = process.env.PORT || 3000 8 | 9 | function createWindow() { 10 | /* 11 | let fileurl = url.format({ 12 | pathname: path.join(__dirname, 'index.html'), 13 | protocol: 'file:', 14 | slashes: true 15 | }) 16 | */ 17 | let fileurl = `http://localhost:${PORT}` 18 | win = new BrowserWindow(fileurl, "Mist running in Tau", 800, 600) 19 | // and load the index.html of the app. 20 | win.loadUrl('index.html') 21 | } 22 | 23 | app.on('ready', createWindow) -------------------------------------------------------------------------------- /app/store/InitialState.js: -------------------------------------------------------------------------------- 1 | 2 | let txCount = 0//await web3.eth.getTransactionCount(accounts[0]) 3 | let tx = { 4 | //"nonce": txCount, 5 | //"from": '0xf17f52151EbEF6C7334FAD080c5704D77216b732', 6 | //"to": '0xC5fdf4076b8F3A5357c5E395ab970B5B54098Fef', 7 | // "gas": "0x76c0", // 30400 8 | //"data": '', 9 | //"gasPrice": "0x9184e72a000", // 10000000000000 10 | //"value": "1000000000000000000" //web3Local.utils.toWei('1.0', 'ether') 11 | } 12 | 13 | let mockTabs = [ 14 | { _id: 'browser', url: 'https://www.stateofthedapps.com', redirect: 'https://www.stateofthedapps.com', position: 0 }, 15 | { _id: '2', url: 'http://www.ethereum.org', redirect: 'http://www.ethereum.org', position: 2 }, 16 | { _id: '3', url: 'https://github.com/ethereum/mist-ui-react', redirect: 'http://www.github.com/philipplgh/mist-react-ui', position: 3 }, 17 | { _id: '4', url: 'http://www.example.com', redirect: 'http://www.example.com', position: 4 } 18 | ].map(tab => {tab.id = tab._id; return tab}) 19 | //let tabs = Tabs.array.sort(el => el.position) 20 | 21 | let suggestedDapps = [{ 22 | name: 'Crypto Kitties', 23 | banner: 'https://www.cryptokitties.co/images/kitty-eth.svg', 24 | description: 'Lorem Ipsum dolor amet sun Lorem Ipsum dolor amet sun Lorem Ipsum dolor amet sun', 25 | url: 'https://www.cryptokitties.co/' 26 | }] 27 | 28 | const initialState = { 29 | 30 | newTx: null, //tx //required by SendTx popup 31 | txs: [], //required by TxHistory popup 32 | 33 | tabs: mockTabs, 34 | suggestedDapps, 35 | 36 | accounts: [], 37 | 38 | nodes: { 39 | active: 'remote', 40 | network: 'main', 41 | changingNetwork: false, 42 | remote: { 43 | client: 'infura', 44 | blockNumber: 100, // if < 1000 NodeInfo will display "connecting.." 45 | timestamp: 0 46 | }, 47 | local: { 48 | client: 'geth', 49 | blockNumber: 1, 50 | timestamp: 0, 51 | syncMode: 'fast', 52 | sync: { 53 | connectedPeers: 0, 54 | currentBlock: 0, 55 | highestBlock: 0, 56 | knownStates: 0, 57 | pulledStates: 0, 58 | startingBlock: 0 59 | } 60 | } 61 | }, 62 | 63 | settings: { 64 | etherPriceUSD: '16', 65 | browser: { 66 | whitelist: 'enabled' 67 | } 68 | } 69 | 70 | } 71 | 72 | module.exports = initialState -------------------------------------------------------------------------------- /app/store/SyncedReduxStore.js: -------------------------------------------------------------------------------- 1 | // export default Helpers.isMist() ? window.store.getState().nodes : window.store 2 | const { createStore, applyMiddleware } = require('redux') 3 | const ReduxThunk = require('redux-thunk') 4 | 5 | const initialState = require('./InitialState') 6 | 7 | /* 8 | const Web3 = require('web3') 9 | let Ganache = new Web3.providers.HttpProvider("HTTP://127.0.0.1:7545") 10 | var web3 = new Web3(Ganache); 11 | //const web3Remote = new Web3(new Web3.providers.WebsocketProvider('wss://rinkeby.infura.io/ws/mist')); 12 | */ 13 | /* 14 | ethereum: { 15 | http: { 16 | Main: 'https://mainnet.infura.io/mist', 17 | Ropsten: 'https://ropsten.infura.io/mist', 18 | Rinkeby: 'https://rinkeby.infura.io/mist', 19 | Kovan: 'https://kovan.infura.io/mist' 20 | }, 21 | websockets: { 22 | Main: 'wss://mainnet.infura.io/ws/mist', 23 | Ropsten: 'wss://ropsten.infura.io/ws/mist', 24 | Rinkeby: 'wss://rinkeby.infura.io/ws/mist', 25 | Kovan: 'wss://kovan.infura.io/ws/mist' 26 | } 27 | }, 28 | ipfs: { 29 | gateway: 'https://ipfs.infura.io', 30 | rpc: 'https://ipfs.infura.io:5001' 31 | } 32 | */ 33 | // const web3Remote = new Web3(new Web3.providers.WebsocketProvider('wss://mainnet.infura.io/ws/mist')); 34 | 35 | /* 36 | (async function fetchAccounts(){ 37 | let _accounts = await web3.eth.getAccounts() 38 | _accounts.forEach(async (acc) => { 39 | //let balance = await web3Remote.eth.getBalance(acc) 40 | let balance = await web3.eth.getBalance(acc) 41 | store.dispatch({ 42 | type: 'ADD_ACCOUNT', 43 | payload: { 44 | address: acc, 45 | balance: balance 46 | } 47 | }) 48 | }) 49 | console.log('received accounts', _accounts) 50 | })() 51 | */ 52 | 53 | function mistApp(state = initialState, action) { 54 | // For now, don't handle any actions 55 | // and just return the state given to us. 56 | switch (action.type) { 57 | case 'SET_LOCAL_PEERCOUNT': 58 | let newState = Object.assign({}, state) 59 | newState.nodes.local.sync.connectedPeers = action.payload 60 | return newState 61 | case 'SET_REMOTE_BLOCK': { 62 | let newState = Object.assign({}, state) 63 | let blockHeader = action.payload 64 | newState.nodes.remote.blockNumber = blockHeader.number 65 | newState.nodes.remote.timestamp = blockHeader.timestamp 66 | return newState 67 | } 68 | case 'ADD_TAB': { 69 | let newState = Object.assign({}, state) 70 | let tab = action.payload 71 | let tabs = [...state.tabs, tab] 72 | newState.tabs = tabs 73 | return newState 74 | } 75 | case 'ADD_ACCOUNT': { 76 | console.log('reducer called: add account') 77 | let newState = Object.assign({}, state) 78 | let acc = action.payload 79 | let accounts = [...state.accounts, acc] 80 | newState.accounts = accounts 81 | return newState 82 | } 83 | case 'SET_TX': { 84 | let newState = Object.assign({}, state) 85 | let tx = action.payload 86 | newState.newTx = tx 87 | return newState 88 | } 89 | case 'EDIT_TAB': { 90 | /* 91 | let newState = Object.assign({}, state) 92 | let tab = action.payload 93 | let tabs = [...state.tabs] // copy tabs state 94 | let tabIdx = tabs.findIndex(t => (t.id === tab.id)) // find changed item 95 | let tabM = { 96 | ...tabs[tabIdx], // create copy of changed item 97 | icon: icon // & modify copy 98 | } 99 | tabs[tabIdx] = tabM // write changes to new tabs state 100 | return { 101 | tabs: tabs 102 | } 103 | */ 104 | } 105 | default: 106 | return state 107 | } 108 | } 109 | const store = createStore(mistApp /*, applyMiddleware(ReduxThunk)*/) 110 | 111 | let _accounts = [ 112 | '0xF5A5d5c30BfAC14bf207b6396861aA471F9A711D', 113 | '0xdf4B9dA0aef26bEE9d55Db34480C722906DB4b02', 114 | '0x944C8763B920fA7a94780B58e78A76Abc87f7cA8', 115 | '0x6564Bcf22912e98960Aa5af5078f4D6f0c01306B', 116 | '0x72317406A02435D967111B190DedB7aecD5c24E1', 117 | '0x4a296FBaB50050BDace36500a96251F3630d3EC6', 118 | '0xb9120Dd975bF62152CF96C474470E96FaF09D094', 119 | '0xe32e6b95957cbDfe668355F11b3EafAA1b537de7', 120 | '0x7b14F76f22B4eC6c3D44a8AB962C31ed5d900aBd', 121 | '0x19BAe22A399E1bFEE0B10419D006E8d112C51e5b', 122 | '0x969912D664477bf4Db7B1Aae743D7BbC3Aa59594', 123 | '0x5793b3709ecdFBBa3019F4a16DC0346aaa20eFE7', 124 | '0x73159c2F51Cc5fa273886Ea047E96C81CC2dBBCE' 125 | ] 126 | _accounts.forEach(acc => { 127 | //let balance = await web3Remote.eth.getBalance(acc) 128 | let balance = 0 129 | store.dispatch({ 130 | type: 'ADD_ACCOUNT', 131 | payload: { 132 | address: acc, 133 | balance: '' + balance 134 | } 135 | }) 136 | }) 137 | 138 | module.exports = store 139 | -------------------------------------------------------------------------------- /app/utils/is.js: -------------------------------------------------------------------------------- 1 | class Is { 2 | renderer() { 3 | // running in a web browser 4 | if (typeof process === 'undefined') return true 5 | // node-integration is disabled 6 | if (!process) return true 7 | // We're in node.js somehow 8 | if (!process.type) return false 9 | return process.type === 'renderer' 10 | } 11 | main() { 12 | return !this.renderer() 13 | } 14 | dev() { 15 | return !!process.env.PORT 16 | //return process.env.NODE_ENV && (process.env.NODE_ENV.trim() === 'development') 17 | } 18 | prod() { 19 | return !this.dev() 20 | } 21 | windows(){ 22 | return process.platform === 'win32' 23 | } 24 | mac(){ 25 | return process.platform === 'darwin' 26 | } 27 | linux(){ 28 | return process.platform === 'linux' 29 | } 30 | } 31 | 32 | module.exports = new Is() -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing to Grid 2 | 3 | First, thanks for your interest! We appreciate your time. 4 | 5 | #### Some background 6 | 7 | this project is broken into several pieces: 8 | 9 | - grid-ui (this repo) - uses the component library to assemble Grid's user interface 10 | - [grid](https://github.com/ethereum/grid) - the desktop app wrapper for grid-ui 11 | - [electron-app-manager](https://github.com/PhilippLgh/electron-app-manager) - handles app updates 12 | 13 | #### How can I contribute? 14 | 15 | - Answer/contribute to other user's open issues. 16 | - Report a bug by opening a GitHub issue. 17 | - Suggest an enhancement by opening a GitHub issue. 18 | - Contribute to documentation by opening a pull request. 19 | - Fix a bug or add a feature by opening a pull request. 20 | - Looking for something to work on? Try filtering for [good first issue](https://github.com/ethereum/grid-ui/labels/good%20first%20issue) tags. 21 | 22 | #### Adding new features 23 | 24 | Before spending the time on a new feature that isn't already requested in an issue, please open a new issue to suggest the enhancement. The Grid team will let you know whether the proposed feature fits into the broader vision. 25 | 26 | #### Reporting bugs 27 | 28 | Before filing, please search for related issues and contribute to existing discussions if appropriate. If no bug resembles yours, do your best to fill out the new issue template, including detailed steps to reproduce the issue. 29 | 30 | #### Pull Requests 31 | 32 | - Please fill out the PR template! Your answers will help us review and merge your code more quickly. 33 | - Use your linter! If your editor isn't configured with eslint, run it in a terminal window with `yarn lint:watch` or `npm run lint:watch`. 34 | - Use [conventional commits](https://www.conventionalcommits.org/) to help us evaluate semantic versioning needs, e.g. `fix:`, `feat:`, `docs:`, etc. commit prefixes. 35 | - [Reference related issues](https://help.github.com/articles/closing-issues-using-keywords/) when appropriate, e.g. "Closes #13" in a PR description. 36 | -------------------------------------------------------------------------------- /i18n/app.en.i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "loading": "Loading...", 4 | "offline": "Can't connect, are you offline?", 5 | "logginIn": "Logging in..." 6 | }, 7 | "error": { 8 | "insufficientRights": "You don't have enough rights for this action." 9 | }, 10 | "buttons": { 11 | "ok": "OK", 12 | "cancel": "Cancel", 13 | "save": "Save", 14 | "edit": "Edit", 15 | "send": "Send", 16 | "next": "Next", 17 | "previous": "Previous", 18 | "back": "Back", 19 | "skip": "Skip", 20 | "sending": "Sending...", 21 | "create": "Create", 22 | "tryToReconnect": "Try to reconnect", 23 | "stayAnonymous": "Stay anonymous", 24 | "authorize": "Authorize" 25 | }, 26 | "commonWords": { 27 | "you": "You", 28 | "send": "Send", 29 | "or": "or", 30 | "of": "of", 31 | "with": "with", 32 | "and": "and", 33 | "on": "on", 34 | "per": "per" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/grid-ui/e686c30738e6e1707b513233d9a59597eb06f652/icons/icon.icns -------------------------------------------------------------------------------- /icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/grid-ui/e686c30738e6e1707b513233d9a59597eb06f652/icons/icon.ico -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/grid-ui/e686c30738e6e1707b513233d9a59597eb06f652/icons/icon.png -------------------------------------------------------------------------------- /icons/icon2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/grid-ui/e686c30738e6e1707b513233d9a59597eb06f652/icons/icon2x.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grid-ui", 3 | "version": "1.6.1", 4 | "private": true, 5 | "main": "./app/shell-electron.js", 6 | "homepage": "./", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ethereum/grid-ui.git" 10 | }, 11 | "scripts": { 12 | "analyze-modules": "source-map-explorer build/static/js/1.*", 13 | "analyze": "source-map-explorer build/static/js/main.*", 14 | "start": "cross-env PORT=3080 CI=false BROWSER=none react-scripts start", 15 | "_build": "react-scripts build", 16 | "build": "react-scripts-ext build app", 17 | "package": "react-scripts-ext package", 18 | "deploy": "react-scripts-ext deploy", 19 | "test": "react-scripts test --env=jsdom", 20 | "eject": "react-scripts eject", 21 | "lint:watch": "esw -w src --ext .jsx --ext .js", 22 | "lint": "eslint src --ext .jsx --ext .js", 23 | "dev:tools": "remotedev & (sleep 3 && open http://localhost:8000)" 24 | }, 25 | "dependencies": { 26 | "@material-ui/core": "^3.9.2", 27 | "@material-ui/icons": "^3.0.2", 28 | "ansi-to-react": "^5.0.0", 29 | "bignumber.js": "^7.2.1", 30 | "bn.js": "^4.11.8", 31 | "classnames": "^2.2.6", 32 | "codemirror": "^5.48.0", 33 | "ethereum-react-components": "^1.13.11", 34 | "lodash": "^4.17.13", 35 | "moment": "^2.24.0", 36 | "notistack": "^0.9.0", 37 | "numeral": "^2.0.6", 38 | "prop-types": "^15.6.2", 39 | "react": "^16.4.1", 40 | "react-codemirror2": "^6.0.0", 41 | "react-desktop": "^0.3.9", 42 | "react-dom": "^16.4.1", 43 | "react-md-spinner": "^0.3.0", 44 | "react-minimal-pie-chart": "^3.5.0", 45 | "react-redux": "^5.0.7", 46 | "react-scripts": "^2.1.1", 47 | "redux": "^4.0.0", 48 | "redux-persist": "^5.10.0", 49 | "redux-thunk": "^2.3.0", 50 | "remote-redux-devtools": "^0.5.16", 51 | "semver": "^5.6.0", 52 | "styled-components": "^4.1.3", 53 | "web3": "^1.0.0-beta.36", 54 | "xterm": "^3.14.5" 55 | }, 56 | "ignore": {}, 57 | "devDependencies": { 58 | "babel-register": "^6.26.0", 59 | "colors": "^1.3.0", 60 | "create-react-app-extensions": "^1.1.1", 61 | "cross-env": "^5.2.0", 62 | "dotenv": "^6.0.0", 63 | "electron": "^2.0.8", 64 | "eslint-config-airbnb": "^17.1.0", 65 | "eslint-watch": "^4.0.2", 66 | "husky": "^1.3.1", 67 | "lint-staged": "^8.1.1", 68 | "prettier": "^1.16.1", 69 | "react-test-renderer": "^16.5.2", 70 | "remotedev-server": "^0.3.1", 71 | "source-map-explorer": "^1.6.0", 72 | "webpack-bundle-analyzer": "^3.3.2" 73 | }, 74 | "husky": { 75 | "hooks": { 76 | "pre-commit": "lint-staged" 77 | } 78 | }, 79 | "lint-staged": { 80 | "*.{js,jsx,json,css,md}": [ 81 | "prettier --no-semi --single-quote --write", 82 | "git add" 83 | ] 84 | }, 85 | "browserslist": [ 86 | ">0.2%", 87 | "not dead", 88 | "not ie <= 11", 89 | "not op_mini all" 90 | ] 91 | } 92 | -------------------------------------------------------------------------------- /preload/preload-webview.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require('electron') 2 | 3 | 4 | function DOMContentLoaded(event) { 5 | const icon = 6 | document.querySelector('link[rel="apple-touch-icon"]') || 7 | document.querySelector('link[type="image/x-icon"]') || 8 | document.querySelector('link[rel="shortcut"]') || 9 | document.querySelector('link[rel="shortcut icon"]') || 10 | document.querySelector('link[rel="icon"]'); 11 | 12 | if (icon) { 13 | ipcRenderer.sendToHost('favicon', icon.href) 14 | } else { 15 | ipcRenderer.sendToHost('favicon', null) 16 | } 17 | 18 | const appBar = document.querySelector( 19 | 'meta[name="ethereum-dapp-url-bar-style"]' 20 | ); 21 | 22 | if (appBar) { 23 | ipcRenderer.sendToHost('appBar', appBar.content); 24 | } else { 25 | ipcRenderer.sendToHost('appBar', null); 26 | } 27 | 28 | document.removeEventListener('DOMContentLoaded', DOMContentLoaded, false) 29 | } 30 | 31 | document.addEventListener('DOMContentLoaded', DOMContentLoaded, false) 32 | 33 | window.prompt = () => { 34 | console.warn("Mist doesn't support window.prompt()") 35 | } -------------------------------------------------------------------------------- /preload/preload.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development' 2 | 3 | const { ipcRenderer, remote } = require('electron') 4 | 5 | const { dialog } = remote 6 | 7 | const fs = require('fs') 8 | const path = require('path') 9 | 10 | // TODO WARNING not browser compatible -> needs to be mocked 11 | const i18nPath = path.join(__dirname, 'i18n') 12 | const app = JSON.parse(fs.readFileSync(path.join(i18nPath, 'app.en.i18n.json'))) 13 | const mist = JSON.parse( 14 | fs.readFileSync(path.join(i18nPath, 'mist.en.i18n.json')) 15 | ) 16 | window.__i18n = { 17 | ...app, 18 | ...mist 19 | } 20 | 21 | const currentWindow = remote.getCurrentWindow() 22 | 23 | window._mist = { 24 | window: { 25 | getArgs: () => currentWindow.args, 26 | close: () => currentWindow.close() 27 | }, 28 | notification: { 29 | warn: msg => { 30 | dialog.showMessageBox(currentWindow, { 31 | type: 'warning', 32 | buttons: [], 33 | message: `${msg}` 34 | }) 35 | } 36 | } 37 | } 38 | 39 | window.__basedir = __dirname // path.join(__dirname, '..') 40 | 41 | window.__require = function(name) { 42 | if (name === 'ipc') { 43 | return ipcRenderer 44 | } 45 | 46 | return null 47 | } 48 | -------------------------------------------------------------------------------- /public/errors/400.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error 400 5 | 6 | 7 | 8 | 18 |
19 | This URL is not allowed. 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/errors/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error 404 5 | 6 | 7 | 8 | 18 | ﴾๏๏﴿

19 | URL not found. 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/errors/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Error 500 5 | 6 | 7 | 8 | 18 | (ノಠ益ಠ)ノ

19 | Oops.. Something went wrong! 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /public/examples/request-access.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |

hello webview

11 | 18 | 19 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/grid-ui/e686c30738e6e1707b513233d9a59597eb06f652/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | 27 | Grid UI 28 | 29 | 30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /settings.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "openDevTools": [] // 'popup', 'renderer' 3 | } -------------------------------------------------------------------------------- /src/API/Grid.js: -------------------------------------------------------------------------------- 1 | export default window.Mist || {} 2 | -------------------------------------------------------------------------------- /src/API/Helpers.js: -------------------------------------------------------------------------------- 1 | // import LocalStore from './LocalStore' 2 | export class Helpers { 3 | /** 4 | Get the webview from either and ID, or the string "browser" 5 | @method getWebview 6 | @param {String} id The Id of a tab or the string "browser" 7 | */ 8 | // eslint-disable-next-line 9 | getWebview(/* id */) { 10 | // FIXME probably bad practice: 11 | // deprecate this and handle over states 12 | // return $('webview[data-id="' + id + '"]')[0] 13 | } 14 | 15 | /* 16 | see actions.js 17 | async function fetchJson(FETCH_ACTION, url){ 18 | dispatch({ type: `${FETCH_ACTION}:START` }); 19 | try { 20 | let response = await fetch(url) 21 | let json = await response.json(); 22 | return json 23 | } catch (error) { 24 | dispatch({ type: `${FETCH_ACTION}:FAILURE`, error }); 25 | } 26 | } 27 | */ 28 | 29 | // eslint-disable-next-line 30 | getCurrentWebview() { 31 | // var webview = this.getWebview(LocalStore.get('selectedTab')); 32 | // return webview 33 | return null 34 | } 35 | 36 | static isMist() { 37 | return window.mistMode === 'mist' 38 | } 39 | 40 | static isElectron() { 41 | // Renderer process 42 | if ( 43 | typeof window !== 'undefined' && 44 | typeof window.process === 'object' && 45 | window.process.type === 'renderer' 46 | ) { 47 | return true 48 | } 49 | // Main process 50 | if ( 51 | typeof process !== 'undefined' && 52 | typeof process.versions === 'object' && 53 | !!process.versions.electron 54 | ) { 55 | return true 56 | } 57 | // Detect the user agent when the `nodeIntegration` option is set to true 58 | if ( 59 | typeof navigator === 'object' && 60 | typeof navigator.userAgent === 'string' && 61 | navigator.userAgent.indexOf('Electron') >= 0 62 | ) { 63 | return true 64 | } 65 | return false 66 | } 67 | } 68 | 69 | export const is = { 70 | electron: Helpers.isElectron, 71 | mist: Helpers.isMist 72 | } 73 | -------------------------------------------------------------------------------- /src/API/Ipc.js: -------------------------------------------------------------------------------- 1 | const _ipc = window.__require ? window.__require('ipc') : {} 2 | 3 | /* 4 | export default class IPC{ 5 | // TODO use whitelist for valid commands or provide functions instead 6 | static send(msg, args){ 7 | _ipc.send(msg, args) 8 | } 9 | } 10 | */ 11 | 12 | export default _ipc 13 | -------------------------------------------------------------------------------- /src/API/LocalStore.js: -------------------------------------------------------------------------------- 1 | export default window.LocalStore 2 | -------------------------------------------------------------------------------- /src/API/Mist.js: -------------------------------------------------------------------------------- 1 | import ipc from './Ipc' 2 | // import Web3 from 'web3' 3 | // import store from './ReduxStore' 4 | 5 | function showPopup(name, args) { 6 | ipc.send('backendAction_showPopup', { 7 | name, 8 | args 9 | }) 10 | } 11 | 12 | const { _mist } = window 13 | 14 | // let Ganache = new Web3.providers.HttpProvider("HTTP://127.0.0.1:7545") 15 | // var web3 = new Web3(Ganache); 16 | 17 | const MistApi = { 18 | geth: { 19 | getConfig: () => { 20 | return {} 21 | }, 22 | getStatus: () => { 23 | return {} 24 | }, 25 | getLocalBinaries: () => { 26 | return [] 27 | }, 28 | getReleases: () => { 29 | return [] 30 | }, 31 | getLogs: () => { 32 | return [] 33 | }, 34 | setConfig: () => {}, 35 | on: () => {}, 36 | rpc: () => {} 37 | }, 38 | window: { 39 | getArgs() { 40 | let args = {} 41 | if (_mist) { 42 | args = _mist.window.getArgs() 43 | } 44 | return args 45 | } 46 | }, 47 | requestAccount: () => { 48 | // window.mist.requestAccount 49 | }, 50 | setWindowSize(w, h) { 51 | ipc.send('backendAction_setWindowSize', w, h) 52 | }, 53 | closeThisWindow() { 54 | if (_mist) { 55 | _mist.window.close() 56 | } 57 | }, 58 | createAccount(args) { 59 | showPopup('CreateAccount', args) 60 | }, 61 | connectAccount(args) { 62 | showPopup('ConnectAccount', args) 63 | }, 64 | sendTransaction(args) { 65 | showPopup('SendTx', args) 66 | }, 67 | showHistory(args) { 68 | showPopup('TxHistory', args) 69 | }, 70 | createAccountWeb3() { 71 | // return new Promise((resolve, reject) => { 72 | /* 73 | web3.eth.personal.newAccount(pw) 74 | .then(address => { 75 | store.dispatch({ 76 | type: 'ADD_ACCOUNT', 77 | payload: { 78 | address: address, 79 | balance: 0 80 | } 81 | }) 82 | resolve(address) 83 | }) 84 | .catch(err => { 85 | console.log('account could not be created', err) 86 | }) 87 | */ 88 | // }) 89 | }, 90 | // replaces GlobalNotification 91 | notification: { 92 | warn: msg => { 93 | console.log('warn warn', msg) 94 | if (_mist) { 95 | _mist.notification.warn(msg.content) 96 | } 97 | /* 98 | GlobalNotification.warning({ 99 | content: error.message || error, 100 | duration: 5 101 | }); 102 | */ 103 | } 104 | } 105 | } 106 | 107 | // window.Mist is made available by mist-shell 108 | const Mist = window.Mist ? window.Mist : MistApi 109 | 110 | export default Mist 111 | -------------------------------------------------------------------------------- /src/API/ReduxStoreSynced.js: -------------------------------------------------------------------------------- 1 | // export default Helpers.isMist() ? window.store.getState().nodes : window.store 2 | import { createStore, applyMiddleware } from 'redux' 3 | import ReduxThunk from 'redux-thunk' 4 | import ipc from './Ipc' 5 | 6 | const initialState = ipc.sendSync('store:get-state-sync') 7 | 8 | function mistApp(state = initialState /* , action */) { 9 | return state 10 | } 11 | const store = createStore(mistApp, applyMiddleware(ReduxThunk)) 12 | 13 | export default store 14 | -------------------------------------------------------------------------------- /src/API/Web3.js: -------------------------------------------------------------------------------- 1 | export default window.web3 2 | -------------------------------------------------------------------------------- /src/API/i18n.js: -------------------------------------------------------------------------------- 1 | export default window.i18n 2 | -------------------------------------------------------------------------------- /src/API/index.js: -------------------------------------------------------------------------------- 1 | export { default as BigNumber } from 'bignumber.js' 2 | // https://github.com/ethereum/meteor-package-tools/blob/master/ethtools.js 3 | 4 | export class EthTools { 5 | static formatBalance(input) { 6 | return window.web3.utils.fromWei(input, 'ether') 7 | } 8 | 9 | static isAddress(address) { 10 | return window.web3.utils.isAddress(address) 11 | } 12 | } 13 | 14 | export { default as ipc } from './Ipc' 15 | export { Helpers } from './Helpers' 16 | export { default as i18n } from './i18n' 17 | export { default as LocalStore } from './LocalStore' 18 | export { default as web3 } from './Web3' 19 | export { default as Mist } from './Mist' 20 | export { default as Grid } from './Grid' 21 | -------------------------------------------------------------------------------- /src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { MuiThemeProvider } from '@material-ui/core/styles' 4 | import { SnackbarProvider } from 'notistack' 5 | import CssBaseline from '@material-ui/core/CssBaseline' 6 | import { darkTheme, lightTheme } from '../theme' 7 | import HelpFab from './shared/HelpFab' 8 | import ErrorBoundary from './GenericErrorBoundary' 9 | import Plugins from './Plugins' 10 | 11 | export default class NewApp extends Component { 12 | static displayName = 'App' 13 | 14 | static propTypes = { 15 | themeMode: PropTypes.oneOf(['dark', 'light']) 16 | } 17 | 18 | render() { 19 | const { themeMode } = this.props 20 | return ( 21 | 22 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Apps/AppItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withStyles } from '@material-ui/core/styles' 4 | import Card from '@material-ui/core/Card' 5 | import CardHeader from '@material-ui/core/CardHeader' 6 | import CardContent from '@material-ui/core/CardContent' 7 | import CardMedia from '@material-ui/core/CardMedia' 8 | import CardActions from '@material-ui/core/CardActions' 9 | import Badge from '@material-ui/core/Badge' 10 | import Typography from '@material-ui/core/Typography' 11 | import moment from 'moment' 12 | import Button from '../shared/Button' 13 | import Grid from '../../API/Grid' 14 | 15 | const styles = { 16 | card: { 17 | background: '#222428' 18 | }, 19 | media: { 20 | height: 0, 21 | paddingTop: '56.25%' // 16:9 22 | }, 23 | title: { 24 | fontSize: 14 25 | } 26 | } 27 | 28 | class AppItem extends React.Component { 29 | static propTypes = { 30 | classes: PropTypes.object.isRequired, 31 | app: PropTypes.object, 32 | badge: PropTypes.number 33 | } 34 | 35 | static defaultProps = { 36 | badge: 0 37 | } 38 | 39 | state = {} 40 | 41 | handleAppLaunch = () => { 42 | const { app } = this.props 43 | Grid.AppManager.launch(app) 44 | } 45 | 46 | render() { 47 | const { classes, app, badge } = this.props 48 | 49 | const { name, lastUpdated, description, screenshot } = app 50 | 51 | return ( 52 | 53 | 59 | 60 | 61 | {description} 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | } 72 | 73 | export default withStyles(styles)(AppItem) 74 | -------------------------------------------------------------------------------- /src/components/Apps/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Grid from '@material-ui/core/Grid' 3 | import { MuiThemeProvider } from '@material-ui/core/styles' 4 | import AppItem from './AppItem' 5 | import GridAPI from '../../API/Grid' 6 | import { darkTheme } from '../../theme' 7 | 8 | class AppsOverview extends React.Component { 9 | state = {} 10 | 11 | render() { 12 | const apps = GridAPI.AppManager ? GridAPI.AppManager.getAvailableApps() : [] 13 | 14 | return ( 15 | 16 |
17 | 18 | {apps.length ? ( 19 | apps.map(app => ( 20 | 21 | 22 | 23 | )) 24 | ) : ( 25 | no apps found 26 | )} 27 | 28 |
29 |
30 | ) 31 | } 32 | } 33 | 34 | export default AppsOverview 35 | -------------------------------------------------------------------------------- /src/components/GenericErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | import { withStyles } from '@material-ui/core/styles' 5 | import Typography from '@material-ui/core/Typography' 6 | 7 | const styles = () => ({ 8 | stackTrace: { 9 | fontFamily: 'monospace', 10 | padding: '12px' 11 | } 12 | }) 13 | 14 | class ErrorBoundary extends Component { 15 | static propTypes = { 16 | children: PropTypes.node, 17 | classes: PropTypes.object 18 | } 19 | 20 | static getDerivedStateFromError(error) { 21 | return { error } 22 | } 23 | 24 | state = { error: null } 25 | 26 | render() { 27 | const { children, classes } = this.props 28 | const { error } = this.state 29 | 30 | if (error) { 31 | return ( 32 | 33 | Whoops, something went wrong! 34 | 35 | If you would like to report this issue on GitHub, please include the 36 | following stack trace: 37 | 38 | 39 | {error && error.stack} 40 | 41 | 42 | ) 43 | } 44 | 45 | return children 46 | } 47 | } 48 | 49 | export default withStyles(styles)(ErrorBoundary) 50 | 51 | const StyledWrapper = styled.div` 52 | padding: 12px; 53 | ` 54 | -------------------------------------------------------------------------------- /src/components/GenericProvider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import configureStore from '../store' 4 | 5 | const store = configureStore() 6 | 7 | // Wrapper around redux provider implementations 8 | // that allows to use different approaches for renderer-main sync 9 | class Provider extends Component { 10 | static propTypes = { 11 | children: PropTypes.node 12 | } 13 | 14 | render() { 15 | // set props on children: https://stackoverflow.com/a/32371612 16 | const { children } = this.props 17 | 18 | const childrenWithProps = React.Children.map(children, child => 19 | React.cloneElement(child, { ...store.getState() }) 20 | ) 21 | 22 | return
{childrenWithProps}
23 | } 24 | } 25 | 26 | export default Provider 27 | -------------------------------------------------------------------------------- /src/components/NavTabs.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withStyles } from '@material-ui/core/styles' 4 | import AppBar from '@material-ui/core/AppBar' 5 | import CssBaseline from '@material-ui/core/CssBaseline' 6 | import Typography from '@material-ui/core/Typography' 7 | import Tab from '@material-ui/core/Tab' 8 | import Tabs from '@material-ui/core/Tabs' 9 | import PluginsTab from './Plugins' 10 | import AppsTab from './Apps' 11 | 12 | const styles = theme => ({ 13 | root: { 14 | display: 'flex' 15 | }, 16 | appBar: { 17 | zIndex: theme.zIndex.drawer + 1 18 | }, 19 | content: { 20 | flexGrow: 1, 21 | padding: theme.spacing.unit * 3 22 | }, 23 | fullWidth: { 24 | width: '100%' 25 | } 26 | }) 27 | 28 | const TabContainer = withStyles(styles)(props => { 29 | const { children, classes, style } = props 30 | return ( 31 |
32 | {children} 33 |
34 | ) 35 | }) 36 | 37 | TabContainer.propTypes = { 38 | children: PropTypes.node.isRequired, 39 | classes: PropTypes.object, 40 | style: PropTypes.object 41 | } 42 | 43 | class NavTabs extends React.Component { 44 | static propTypes = { 45 | classes: PropTypes.object 46 | } 47 | 48 | state = { 49 | activeTab: 0 50 | } 51 | 52 | handleTabChange = (event, activeTab) => { 53 | this.setState({ activeTab }) 54 | } 55 | 56 | render() { 57 | const { classes } = this.props 58 | const { activeTab } = this.state 59 | 60 | return ( 61 |
62 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 75 | 82 | 83 | 84 | 85 | 86 |
87 | 88 |
89 |
90 |
91 | ) 92 | } 93 | } 94 | 95 | export default withStyles(styles)(NavTabs) 96 | -------------------------------------------------------------------------------- /src/components/Plugins/Metadata/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withStyles } from '@material-ui/core/styles' 4 | import Grid from '@material-ui/core/Grid' 5 | import TextField from '@material-ui/core/TextField' 6 | import { Controlled as CodeMirror } from 'react-codemirror2' 7 | import 'codemirror/lib/codemirror.css' 8 | import 'codemirror/theme/material.css' 9 | import moment from 'moment' 10 | 11 | require('codemirror/mode/javascript/javascript') 12 | 13 | const styles = () => ({ 14 | pluginCode: { 15 | width: 'calc(100vw - 310px)' 16 | } 17 | }) 18 | 19 | class Metadata extends Component { 20 | static propTypes = { 21 | classes: PropTypes.object, 22 | plugin: PropTypes.object.isRequired 23 | } 24 | 25 | renderPluginMetadata = metadata => { 26 | const { classes } = this.props 27 | const { 28 | // name, 29 | // displayName, 30 | // repository, 31 | fileName, 32 | // commit, 33 | publishedDate, 34 | version, 35 | // channel, 36 | size, 37 | // tag, 38 | location, 39 | // remote, 40 | verificationResult 41 | } = metadata 42 | const { signers, /* isTrusted, */ isValid } = verificationResult 43 | 44 | return ( 45 | 46 | {/* 47 | 48 | 56 | 57 | */} 58 | 59 | 67 | 68 | 69 | 77 | 78 | 79 | s.address).join(',')} 84 | className={classes.textField} 85 | margin="normal" 86 | /> 87 | 88 | 89 | 97 | 98 | {/* 99 | 100 | 108 | 109 | */} 110 | 111 | 119 | 120 | 121 | 129 | 130 | 131 | 139 | 140 | 141 | ) 142 | } 143 | 144 | render() { 145 | const { classes, plugin } = this.props 146 | const { metadata } = plugin 147 | 148 | return ( 149 |
150 | {metadata && ( 151 | 152 |

Plugin Details

153 | 154 | {this.renderPluginMetadata(metadata)} 155 | 156 |
157 | )} 158 |

Plugin Code

159 | { 168 | console.log(editor, data, value) 169 | }} 170 | /> 171 |
172 | ) 173 | } 174 | } 175 | 176 | export default withStyles(styles)(Metadata) 177 | -------------------------------------------------------------------------------- /src/components/Plugins/NodeInfo/NodeInfoBox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import moment from 'moment' 3 | import styled, { css } from 'styled-components' 4 | import SettingsInputAntennaIcon from '@material-ui/icons/SettingsInputAntenna' 5 | import OfflineBoltIcon from '@material-ui/icons/OfflineBolt' 6 | import AvTimerIcon from '@material-ui/icons/AvTimer' 7 | import CloudDownloadIcon from '@material-ui/icons/CloudDownload' 8 | import LayersIcon from '@material-ui/icons/Layers' 9 | import PeopleIcon from '@material-ui/icons/People' 10 | import LinearScaleIcon from '@material-ui/icons/LinearScale' 11 | 12 | const numberWithCommas = (val = 0) => { 13 | return val.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',') 14 | } 15 | 16 | const defaultIconProps = { 17 | fontSize: 'inherit' 18 | } 19 | 20 | class NodeInfoBox extends Component { 21 | renderStopped = () => { 22 | return ( 23 | 24 | 25 | Stopped 26 | 27 | ) 28 | } 29 | 30 | renderConnecting = () => { 31 | return ( 32 | 33 | 34 | Connecting... 35 | 36 | ) 37 | } 38 | 39 | renderFindingPeers = () => { 40 | return ( 41 | 42 | 43 | Looking for peers... 44 | 45 | ) 46 | } 47 | 48 | renderSyncStarting = () => { 49 | const { plugin } = this.props 50 | const { active } = plugin 51 | const { peerCount } = active 52 | 53 | return ( 54 |
55 | 56 | 57 | {`${peerCount} peers`} 58 | 59 | 60 | 61 | Sync starting... 62 | 63 |
64 | ) 65 | } 66 | 67 | renderSyncProgress() { 68 | const { plugin } = this.props 69 | const { config, active } = plugin 70 | const { peerCount, sync } = active 71 | const { network } = config 72 | const { highestBlock, currentBlock, startingBlock } = sync 73 | 74 | const formattedCurrentBlock = numberWithCommas(currentBlock) 75 | 76 | const progress = 77 | ((currentBlock - startingBlock) / (highestBlock - startingBlock)) * 100 78 | 79 | return ( 80 |
81 | 82 | 83 | {formattedCurrentBlock} 84 | 85 | 86 | 87 | {`${peerCount} peers`} 88 | 89 | 90 | 91 | 96 | 97 |
98 | ) 99 | } 100 | 101 | renderSynced() { 102 | const { plugin, diffTimestamp } = this.props 103 | const { active, config } = plugin 104 | const { blockNumber, peerCount, timestamp } = active 105 | const { network } = config 106 | 107 | const formattedBlockNumber = numberWithCommas(blockNumber) 108 | 109 | const timeSince = moment.unix(timestamp) 110 | const diff = moment.unix(diffTimestamp).diff(timeSince, 'seconds') 111 | 112 | return ( 113 |
114 | 115 | 116 | {formattedBlockNumber} 117 | 118 | {network !== 'private' && ( 119 | 120 | 121 | {peerCount} peers 122 | 123 | )} 124 | 60 ? 'block-diff orange' : 'block-diff'} 126 | > 127 | { 128 | // TODO: make this i8n compatible 129 | } 130 | 131 | {diff < 120 ? `${diff} seconds` : `${Math.floor(diff / 60)} minutes`} 132 | 133 |
134 | ) 135 | } 136 | 137 | renderStats() { 138 | const { plugin } = this.props 139 | const { active, config } = plugin 140 | const { syncMode, network } = config 141 | const { blockNumber, peerCount, status, sync } = active 142 | const { highestBlock, startingBlock } = sync 143 | 144 | let stats 145 | 146 | if (status === 'STARTED') { 147 | // Case: connecting 148 | stats = this.renderConnecting() 149 | } 150 | if (status === 'CONNECTED') { 151 | if (peerCount === 0) { 152 | // Case: no peers yet 153 | stats = this.renderFindingPeers() 154 | } else { 155 | // Case: connected to peers, but no blocks yet 156 | stats = this.renderSyncStarting() 157 | } 158 | } 159 | if (blockNumber > 0 && blockNumber - 50 > highestBlock) { 160 | // Case: all sync'd up 161 | stats = this.renderSynced() 162 | } else if (startingBlock > 0) { 163 | // Case: show progress 164 | stats = this.renderSyncProgress() 165 | } 166 | if (status === 'STOPPED') { 167 | // Case: node stopped 168 | stats = this.renderStopped() 169 | } 170 | 171 | return ( 172 | 173 | 174 | Local Node 175 | {syncMode} sync 176 | 177 | {stats} 178 | 179 | ) 180 | } 181 | 182 | render() { 183 | const { plugin } = this.props 184 | const { config } = plugin 185 | const { network } = config 186 | return ( 187 | 188 | 189 |
190 | 191 | {network} 192 | 193 | {network !== 'main' && 'Test Network'} 194 | {network === 'main' && 'Network'} 195 | 196 | 197 | {this.renderStats()} 198 |
199 |
200 |
201 | ) 202 | } 203 | } 204 | 205 | export default NodeInfoBox 206 | 207 | const StyledSubmenuContainer = styled.div` 208 | width: 220px; 209 | border-radius: 5px; 210 | z-index: 10000; 211 | cursor: default; 212 | 213 | transition: 150ms linear all, 1ms linear top; 214 | transition-delay: 200ms; 215 | transform: translateY(-11px); 216 | 217 | section { 218 | background-color: rgba(0, 0, 0, 0.75); 219 | backdrop-filter: blur(2px); 220 | width: 100%; 221 | border-radius: 5px; 222 | color: #fff; 223 | position: relative; 224 | } 225 | 226 | /* Apply css arrow to topLeft of box */ 227 | position: absolute; 228 | left: 354px; 229 | top: 38px; 230 | 231 | &::before { 232 | content: ''; 233 | margin-left: -8px; 234 | top: 0; 235 | margin-top: 12px; 236 | display: block; 237 | position: absolute; 238 | width: 0px; 239 | height: 8px * 2.25; 240 | border: 0px solid transparent; 241 | border-width: 8px; 242 | border-left: 0; 243 | border-right-color: rgba(0, 0, 0, 0.78); 244 | } 245 | ` 246 | 247 | const StyledSection = styled.div` 248 | border-top: 1px solid rgba(255, 255, 255, 0.15); 249 | padding: 11px; 250 | &:first-of-type { 251 | border-top: none; 252 | } 253 | ` 254 | 255 | const StyledNetworkTitle = styled.div` 256 | font-weight: 300; 257 | font-size: 24px; 258 | text-transform: capitalize; 259 | ` 260 | 261 | const StyledSubtitle = styled.div` 262 | margin-left: 1px; 263 | font-size: 10px; 264 | color: #aaa; 265 | text-transform: uppercase; 266 | ` 267 | 268 | const StyledPill = styled.span` 269 | display: inline-block; 270 | margin-left: 5px; 271 | font-weight: 300; 272 | font-size: 11px; 273 | background-color: rgba(255, 255, 255, 0.1); 274 | border-radius: 8px; 275 | padding: 2px 6px; 276 | vertical-align: middle; 277 | text-transform: none; 278 | ` 279 | 280 | const colorMainnet = '#7ed321' 281 | const colorTestnet = '#00aafa' 282 | 283 | const StyledTitle = styled.div` 284 | font-size: 18px; 285 | font-weight: 200; 286 | margin-bottom: 6px; 287 | strong { 288 | font-weight: 400; 289 | } 290 | ${props => 291 | !props.testnet && 292 | css` 293 | color: ${colorMainnet}; 294 | `} 295 | ${props => 296 | props.testnet && 297 | css` 298 | color: ${colorTestnet}; 299 | `} 300 | ` 301 | 302 | const StyledProgress = styled.progress` 303 | width: 100%; 304 | border-radius: 3px; 305 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5) inset; 306 | background: rgba(255, 255, 255, 0.1); 307 | height: 5px; 308 | 309 | ${props => 310 | !props.testnet && 311 | css` 312 | &::-webkit-progress-value { 313 | bakground-image: linear-gradient(left, transparent, ${colorMainnet}); 314 | background: ${colorMainnet}; 315 | background-size: cover; 316 | } 317 | `} 318 | 319 | ${props => 320 | props.testnet && 321 | css` 322 | &::-webkit-progress-value { 323 | background-image: linear-gradient(left, transparent, ${colorTestnet}); 324 | background: ${colorTestnet}; 325 | background-size: cover; 326 | } 327 | `} 328 | ` 329 | 330 | const StyledBox = styled.div` 331 | font-family: sans-serif; 332 | ${props => 333 | props.dotLocation && 334 | css` 335 | position: relative; 336 | top: -17px; 337 | `} 338 | 339 | strong { 340 | font-weight: 500; 341 | } 342 | 343 | .orange { 344 | color: orange; 345 | } 346 | 347 | .red { 348 | color: #e81e1e; 349 | } 350 | ` 351 | 352 | const StyledIconRow = styled.div` 353 | margin-bottom: 6px; 354 | display: flex; 355 | align-items: center; 356 | font-size: 13px; 357 | svg { 358 | display: inline-block; 359 | margin-right: 6px; 360 | } 361 | &:last-of-type { 362 | margin-bottom: 0; 363 | } 364 | ` 365 | -------------------------------------------------------------------------------- /src/components/Plugins/NodeInfo/NodeInfoDot.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled, { css, keyframes } from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | import moment from 'moment' 5 | import PieChart from 'react-minimal-pie-chart' 6 | 7 | const colorMainnet = '#7ed321' 8 | const colorTestnet = '#00aafa' 9 | const colorRed = '#de3232' 10 | const colorOrange = 'orange' 11 | 12 | class NodeInfoDot extends Component { 13 | static propTypes = { 14 | plugin: PropTypes.object, 15 | diffTimestamp: PropTypes.number, 16 | isStopped: PropTypes.bool, 17 | /** If component is stickied, apply drop shadow on dot */ 18 | sticky: PropTypes.bool 19 | } 20 | 21 | state = { 22 | pulseColor: '' 23 | } 24 | 25 | componentDidUpdate(prevProps) { 26 | const { plugin } = this.props 27 | const { blockNumber: oldBlockNumber } = prevProps.plugin.active 28 | const { blockNumber: newBlockNumber } = plugin.active 29 | if ((Number(newBlockNumber) || 0) > (Number(oldBlockNumber) || 0)) { 30 | this.pulseForNewBlock() 31 | } 32 | } 33 | 34 | determineDotColor = () => { 35 | const { plugin, isStopped } = this.props 36 | const { config, active } = plugin 37 | const { network } = config 38 | const { blockNumber } = active 39 | 40 | let dotColor 41 | dotColor = network === 'main' ? colorMainnet : colorTestnet 42 | if (this.secondsSinceLastBlock() > 120 || !blockNumber) { 43 | dotColor = colorOrange 44 | } 45 | if (isStopped) { 46 | dotColor = colorRed 47 | } 48 | return dotColor 49 | } 50 | 51 | pulseForNewBlock = () => { 52 | const { plugin } = this.props 53 | const { config } = plugin 54 | const { network } = config 55 | 56 | const pulseColor = network === 'main' ? 'green' : 'blue' 57 | 58 | this.setState({ pulseColor }, () => { 59 | setTimeout(() => { 60 | this.setState({ pulseColor: '' }) 61 | }, 2000) 62 | }) 63 | } 64 | 65 | secondsSinceLastBlock = () => { 66 | const { plugin, diffTimestamp } = this.props 67 | const { timestamp } = plugin 68 | const lastBlock = moment.unix(timestamp) // eslint-disable-line 69 | return moment.unix(diffTimestamp).diff(lastBlock, 'seconds') 70 | } 71 | 72 | render() { 73 | const { plugin, sticky } = this.props 74 | const { pulseColor } = this.state 75 | const { active, config } = plugin 76 | const { blockNumber } = active 77 | const { network } = config 78 | 79 | const { sync } = active 80 | const { highestBlock, currentBlock, startingBlock } = sync 81 | const progress = 82 | ((currentBlock - startingBlock) / (highestBlock - startingBlock)) * 100 83 | 84 | const dotColor = this.determineDotColor() 85 | 86 | return ( 87 |
88 | 95 | {currentBlock > 0 && dotColor !== colorRed && ( 96 | 100 ? 'orange' : 'red' 108 | } 109 | ]} 110 | /> 111 | )} 112 | 113 |
114 | ) 115 | } 116 | } 117 | 118 | export default NodeInfoDot 119 | 120 | const beaconOrange = keyframes` 121 | 0% { 122 | box-shadow: 0 0 0 0 rgba(255, 165, 0, 0.4); 123 | } 124 | 70% { 125 | box-shadow: 0 0 0 10px rgba(255, 165, 0, 0); 126 | } 127 | 100% { 128 | box-shadow: 0 0 0 0 rgba(255, 165, 0, 0); 129 | } 130 | ` 131 | 132 | const beaconGreen = keyframes` 133 | 0% { 134 | box-shadow: 0 0 0 0 rgba(36, 195, 58, 0.4); 135 | } 136 | 70% { 137 | box-shadow: 0 0 0 10px rgba(36, 195, 58, 0); 138 | } 139 | 100% { 140 | box-shadow: 0 0 0 0 rgba(36, 195, 58, 0); 141 | } 142 | ` 143 | 144 | const beaconBlue = keyframes` 145 | 0% { 146 | box-shadow: 0 0 0 0 rgba(0, 170, 250, 0.4); 147 | } 148 | 70% { 149 | box-shadow: 0 0 0 10px rgba(0, 170, 250, 0); 150 | } 151 | 100% { 152 | box-shadow: 0 0 0 0 rgba(0, 170, 250, 0); 153 | } 154 | ` 155 | 156 | const StyledLight = styled.div` 157 | position: relative; 158 | z-index: 1; 159 | height: 16px; 160 | width: 16px; 161 | border-radius: 50%; 162 | transition: background-color ease-in-out 2s; 163 | 164 | svg { 165 | position: absolute; 166 | top: 0; 167 | left: 0; 168 | z-index: 2; 169 | height: 16px; 170 | } 171 | 172 | ${props => 173 | props.sticky && 174 | css` 175 | box-shadow: inset rgba(0, 0, 0, 0.3) 0 1px 3px; 176 | `} 177 | 178 | ${props => 179 | props.pulseColor === 'orange' && 180 | css` 181 | animation: ${beaconOrange} ease-in-out; 182 | animation-duration: 2s; 183 | `} 184 | 185 | ${props => 186 | props.pulseColor === 'green' && 187 | css` 188 | animation: ${beaconGreen} ease-in-out; 189 | animation-duration: 2s; 190 | `} 191 | 192 | ${props => 193 | props.pulseColor === 'blue' && 194 | css` 195 | animation: ${beaconBlue} ease-in-out; 196 | animation-duration: 2s; 197 | `} 198 | ` 199 | -------------------------------------------------------------------------------- /src/components/Plugins/NodeInfo/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | import classNames from 'classnames' 5 | import { connect } from 'react-redux' 6 | import moment from 'moment' 7 | 8 | import NodeInfoDot from './NodeInfoDot' 9 | import NodeInfoBox from './NodeInfoBox' 10 | 11 | class NodeInfo extends Component { 12 | static displayName = 'NodeInfo' 13 | 14 | static propTypes = { 15 | pluginState: PropTypes.object, 16 | selectedPlugin: PropTypes.string 17 | } 18 | 19 | state = { 20 | showSubmenu: false, 21 | diffTimestamp: moment().unix() 22 | } 23 | 24 | componentDidMount() { 25 | // diffTimestamp used to calculate time since last block 26 | this.diffInterval = setInterval(() => { 27 | this.setState({ diffTimestamp: moment().unix() }) 28 | }, 1000) 29 | } 30 | 31 | componentWillUnmount() { 32 | clearInterval(this.diffInterval) 33 | } 34 | 35 | render() { 36 | const { pluginState, selectedPlugin } = this.props 37 | const plugin = pluginState[selectedPlugin] 38 | if (!plugin) return null 39 | 40 | const { diffTimestamp, showSubmenu, sticky } = this.state 41 | const { network } = plugin 42 | 43 | const nodeInfoClass = classNames({ 44 | 'node-mainnet': network === 'main', 45 | 'node-testnet': network !== 'main', 46 | sticky 47 | }) 48 | 49 | return ( 50 | 51 |
this.setState({ sticky: !sticky })} 55 | onMouseEnter={() => this.setState({ showSubmenu: true })} 56 | onMouseLeave={() => this.setState({ showSubmenu: sticky })} 57 | role="button" 58 | tabIndex={0} 59 | > 60 | 66 | {showSubmenu && ( 67 | 68 | )} 69 |
70 |
71 | ) 72 | } 73 | } 74 | 75 | function mapStateToProps(state) { 76 | return { 77 | pluginState: state.plugin, 78 | selectedPlugin: state.plugin.selected 79 | } 80 | } 81 | 82 | export default connect(mapStateToProps)(NodeInfo) 83 | 84 | const StyledNode = styled.div` 85 | cursor: default; 86 | display: inline-block; 87 | font-size: 0.9em; 88 | color: #827a7a; 89 | 90 | #node-info { 91 | margin: 0 8px; 92 | -webkit-app-region: no-drag; 93 | 94 | &:focus { 95 | outline: 0; 96 | } 97 | } 98 | ` 99 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/AboutPlugin.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { connect } from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import { withStyles } from '@material-ui/core/styles' 5 | import Typography from '@material-ui/core/Typography' 6 | import Grid from '@material-ui/core/Grid' 7 | import GridAPI from '../../../API/Grid' 8 | import AppItem from '../../Apps/AppItem' 9 | import DependencyCard from './DependencyCard' 10 | 11 | const styles = { 12 | spacing: { 13 | marginTop: 30 14 | }, 15 | headerText: { 16 | fontWeight: 'bold' 17 | }, 18 | link: { 19 | display: 'block', 20 | color: 'white', 21 | textDecoration: 'none', 22 | '&:hover': { 23 | textDecoration: 'underline' 24 | } 25 | } 26 | } 27 | 28 | class AboutPlugin extends Component { 29 | static propTypes = { 30 | plugin: PropTypes.object.isRequired, 31 | pluginState: PropTypes.object.isRequired, 32 | classes: PropTypes.object.isRequired 33 | } 34 | 35 | state = { 36 | gridApps: [] 37 | } 38 | 39 | async componentDidMount() { 40 | const gridApps = await GridAPI.AppManager.getAllApps() 41 | this.setState({ gridApps }) 42 | } 43 | 44 | renderLinks(links, name) { 45 | const { classes } = this.props 46 | 47 | if (!links) { 48 | return null 49 | } 50 | 51 | const renderList = links.map(link => ( 52 | 53 | {link.name} 54 | 55 | )) 56 | 57 | return ( 58 |
59 | 60 | {name} 61 | 62 | {renderList} 63 |
64 | ) 65 | } 66 | 67 | renderApps() { 68 | const { plugin, classes, pluginState } = this.props 69 | const { gridApps } = this.state 70 | const { apps } = plugin.about 71 | 72 | if (!apps || !gridApps) { 73 | return null 74 | } 75 | const renderList = ( 76 | 77 | {apps.map(app => { 78 | const gridApp = gridApps.find(thisApp => thisApp.url === app.url) 79 | if (gridApp) { 80 | // Overwrite `dependencies` key with one specified in plugin so 81 | // plugin can have priority in launching with its own settings. 82 | const finalApp = { ...gridApp } 83 | if (app.dependencies) { 84 | finalApp.dependencies = app.dependencies 85 | } 86 | let badge = 0 87 | if (pluginState[plugin.name].appBadges[gridApp.id]) { 88 | badge = pluginState[plugin.name].appBadges[gridApp.id] 89 | } 90 | return ( 91 | 92 | 93 | 94 | ) 95 | } 96 | return null 97 | })} 98 | 99 | ) 100 | 101 | return ( 102 |
103 | 108 | Apps 109 | 110 | {renderList} 111 |
112 | ) 113 | } 114 | 115 | renderDependencies = dependencies => { 116 | const { classes } = this.props 117 | const { runtime: runtimeDependencies } = dependencies 118 | return ( 119 | 120 | 125 | Dependencies 126 | 127 | 128 | 129 | {runtimeDependencies.map(dependency => ( 130 | 131 | ))} 132 | 133 | 134 | 135 | ) 136 | } 137 | 138 | render() { 139 | const { plugin, classes } = this.props 140 | const { about, dependencies } = plugin 141 | 142 | if (!about) return

Plugin has no about data.

143 | 144 | const { description, links, community, docs } = about 145 | return ( 146 |
147 | {description && ( 148 |
149 |
150 | 155 | Description 156 | 157 |
158 |
159 | {description} 160 |
161 |
162 | )} 163 | 164 | 165 | {this.renderLinks(links, 'Links')} 166 | 167 | 168 | {this.renderLinks(docs, 'Documentation')} 169 | 170 | 171 | {this.renderLinks(community, 'Community')} 172 | 173 | 174 | {dependencies && this.renderDependencies(dependencies)} 175 | {this.renderApps()} 176 |
177 | ) 178 | } 179 | } 180 | 181 | function mapStateToProps(state) { 182 | return { 183 | pluginState: state.plugin 184 | } 185 | } 186 | 187 | export default connect(mapStateToProps)(withStyles(styles)(AboutPlugin)) 188 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/DependencyCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withStyles } from '@material-ui/core/styles' 4 | import Card from '@material-ui/core/Card' 5 | import CardContent from '@material-ui/core/CardContent' 6 | import CardMedia from '@material-ui/core/CardMedia' 7 | import CardActions from '@material-ui/core/CardActions' 8 | import CardHeader from '@material-ui/core/CardHeader' 9 | import Typography from '@material-ui/core/Typography' 10 | import CheckIcon from '@material-ui/icons/Check' 11 | import Button from '../../shared/Button' 12 | import Grid from '../../../API/Grid' 13 | 14 | // FIXME don't hotlink 15 | const JAVA_LOGO = 16 | 'https://upload.wikimedia.org/wikipedia/de/thumb/e/e1/Java-Logo.svg/364px-Java-Logo.svg.png' 17 | 18 | const styles = { 19 | card: { 20 | maxWidth: 230, 21 | background: '#222428' 22 | }, 23 | media: { 24 | height: 0, 25 | paddingTop: '75%', // 4:3 26 | padding: '10%' 27 | }, 28 | title: { 29 | fontSize: 14 30 | } 31 | } 32 | 33 | class AppItem extends React.Component { 34 | static propTypes = { 35 | classes: PropTypes.object.isRequired, 36 | dependency: PropTypes.object.isRequired 37 | } 38 | 39 | state = {} 40 | 41 | handleAppLaunch = () => { 42 | Grid.openExternalLink( 43 | 'http://www.oracle.com/technetwork/java/javase/downloads/index.html' 44 | ) 45 | } 46 | 47 | render() { 48 | const { classes, dependency } = this.props 49 | const { name, type, version } = dependency 50 | const description = `Required version: ${type} ${version} 64Bit` 51 | const logo = name === 'Java' ? JAVA_LOGO : undefined 52 | return ( 53 | 54 | {logo ? ( 55 | 60 | ) : ( 61 | 62 | )} 63 | 64 | {description} 65 | 66 | 67 | {Grid.platform.hasRuntime(dependency) ? ( 68 | 75 | {name} Installed 76 | 77 | ) : ( 78 | 79 | )} 80 | 81 | 82 | ) 83 | } 84 | } 85 | 86 | export default withStyles(styles)(AppItem) 87 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/DynamicConfigForm/FlagPreview.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { connect } from 'react-redux' 3 | import debounce from 'lodash/debounce' 4 | import { withSnackbar } from 'notistack' 5 | import PropTypes from 'prop-types' 6 | import TextField from '@material-ui/core/TextField' 7 | import Button from '@material-ui/core/Button' 8 | import FormGroup from '@material-ui/core/FormGroup' 9 | import FormControlLabel from '@material-ui/core/FormControlLabel' 10 | import Checkbox from '@material-ui/core/Checkbox' 11 | import Grid from '@material-ui/core/Grid' 12 | import { 13 | dismissFlagWarning, 14 | restoreDefaultSettings, 15 | setCustomFlags 16 | } from '../../../../store/plugin/actions' 17 | import { getDefaultFlags } from '../../../../lib/utils' 18 | 19 | class FlagPreview extends Component { 20 | static propTypes = { 21 | config: PropTypes.object, 22 | plugin: PropTypes.object, 23 | pluginName: PropTypes.string, 24 | isPluginRunning: PropTypes.bool, 25 | flags: PropTypes.array, 26 | isEditingFlags: PropTypes.bool, 27 | toggleEditGeneratedFlags: PropTypes.func, 28 | dispatch: PropTypes.func, 29 | showWarning: PropTypes.bool, 30 | enqueueSnackbar: PropTypes.func, 31 | closeSnackbar: PropTypes.func 32 | } 33 | 34 | constructor(props) { 35 | super(props) 36 | // NOTE: for performance, form fields are populated by local state. 37 | // Redux state doesn't need to update on every keystroke. 38 | this.updateRedux = debounce(this.updateRedux, 500) 39 | this.defaultFlags = getDefaultFlags(props.plugin, props.config).join(' ') 40 | this.state = { 41 | flags: props.flags.join(' '), 42 | warningHasBeenShown: false 43 | } 44 | } 45 | 46 | componentDidUpdate() { 47 | const { flags: localFlags, warningHasBeenShown } = this.state 48 | const { flags, isEditingFlags, showWarning } = this.props 49 | 50 | if (isEditingFlags && showWarning && !warningHasBeenShown) { 51 | this.handleShowWarning() 52 | } 53 | 54 | // If props update from outside of this component, 55 | // e.g. restore defaults, update local state 56 | const newFlags = flags.join(' ') 57 | if (localFlags !== newFlags && !isEditingFlags) { 58 | this.updateFlags(newFlags) 59 | } 60 | } 61 | 62 | componentWillUnmount() { 63 | this.updateRedux.cancel() 64 | } 65 | 66 | updateFlags = flags => { 67 | this.setState({ flags }) 68 | } 69 | 70 | handleShowWarning = () => { 71 | const { closeSnackbar, enqueueSnackbar } = this.props 72 | 73 | this.setState({ warningHasBeenShown: true }, () => { 74 | enqueueSnackbar("Use caution! Don't take flags from strangers.", { 75 | variant: 'warning', 76 | onClose: () => { 77 | this.dismissFlagWarning() 78 | }, 79 | action: key => ( 80 | 81 | 90 | 91 | ) 92 | }) 93 | }) 94 | } 95 | 96 | toggleEdit = event => { 97 | const { toggleEditGeneratedFlags } = this.props 98 | toggleEditGeneratedFlags(event.target.checked) 99 | } 100 | 101 | handleChange = event => { 102 | const flags = event.target.value 103 | const { pluginName } = this.props 104 | this.setState({ flags }) 105 | this.updateRedux(pluginName, flags) 106 | } 107 | 108 | updateRedux = (pluginName, flags) => { 109 | const { dispatch } = this.props 110 | dispatch(setCustomFlags(pluginName, flags.split(' '))) 111 | } 112 | 113 | dismissFlagWarning = () => { 114 | const { dispatch } = this.props 115 | dispatch(dismissFlagWarning()) 116 | } 117 | 118 | handleRestoreDefaultSettings = () => { 119 | const { dispatch, plugin, closeSnackbar, enqueueSnackbar } = this.props 120 | this.setState({ flags: this.defaultFlags }, () => { 121 | dispatch(restoreDefaultSettings(plugin)) 122 | enqueueSnackbar('Default settings restored!', { 123 | variant: 'success', 124 | onClose: () => { 125 | this.dismissFlagWarning() 126 | }, 127 | action: key => ( 128 | 129 | 138 | 139 | ) 140 | }) 141 | }) 142 | } 143 | 144 | render() { 145 | const { isEditingFlags, isPluginRunning } = this.props 146 | const { flags } = this.state 147 | 148 | return ( 149 | 150 | 151 | 160 | 161 | 162 | 163 | 172 | } 173 | label="Use custom flags" 174 | /> 175 | 176 | 177 | 184 | 185 | 186 | 187 | ) 188 | } 189 | } 190 | 191 | function mapStateToProps(state) { 192 | return { 193 | pluginName: state.plugin.selected, 194 | showWarning: state.plugin.showCustomFlagWarning 195 | } 196 | } 197 | 198 | export default connect(mapStateToProps)(withSnackbar(FlagPreview)) 199 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/DynamicConfigForm/FormItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import debounce from 'lodash/debounce' 5 | import TextField from '@material-ui/core/TextField' 6 | import InputAdornment from '@material-ui/core/InputAdornment' 7 | import IconButton from '@material-ui/core/IconButton' 8 | import FolderOpenIcon from '@material-ui/icons/FolderOpen' 9 | import Select from '../../../shared/Select' 10 | import { Grid as GridAPI } from '../../../../API' 11 | 12 | class DynamicConfigFormItem extends Component { 13 | static propTypes = { 14 | itemKey: PropTypes.string, 15 | itemValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 16 | item: PropTypes.object, 17 | pluginState: PropTypes.object, 18 | pluginName: PropTypes.string, 19 | isPluginRunning: PropTypes.bool, 20 | handlePluginConfigChanged: PropTypes.func, 21 | isEditingFlags: PropTypes.bool 22 | } 23 | 24 | constructor(props) { 25 | super(props) 26 | this.inputOpenFileRef = React.createRef() 27 | 28 | // NOTE: for performance, form fields are populated by local state. 29 | // Redux state doesn't need to update on every keystroke. 30 | this.updateRedux = debounce(this.updateRedux, 500) 31 | this.state = { 32 | fieldValue: (props.itemValue || '').toString() 33 | } 34 | } 35 | 36 | componentDidUpdate(prevProps) { 37 | const { itemValue } = this.props 38 | if (prevProps.itemValue !== itemValue) { 39 | this.updateField(itemValue) 40 | } 41 | } 42 | 43 | componentWillUnmount() { 44 | this.updateRedux.cancel() 45 | } 46 | 47 | updateField = fieldValue => { 48 | this.setState({ fieldValue }) 49 | } 50 | 51 | showOpenDialog = key => async event => { 52 | const { showOpenDialog } = GridAPI 53 | // If we don't have showOpenDialog from Grid, 54 | // return true to continue with native file dialog 55 | if (!showOpenDialog) return true 56 | // Continue with Grid.showOpenDialog() 57 | event.preventDefault() 58 | const { pluginState, pluginName, item } = this.props 59 | const { type } = item 60 | const defaultPath = pluginState[pluginName].config[key] 61 | const pathType = type.replace('_multiple', '') 62 | const selectMultiple = type.includes('multiple') 63 | const path = await showOpenDialog(pathType, selectMultiple, defaultPath) 64 | this.handleChange(key, path) 65 | return null 66 | } 67 | 68 | handleChange = (key, value) => { 69 | this.setState({ fieldValue: value }) 70 | this.updateRedux(key, value) 71 | } 72 | 73 | updateRedux = (key, value) => { 74 | const { handlePluginConfigChanged } = this.props 75 | handlePluginConfigChanged(key, value) 76 | } 77 | 78 | render() { 79 | const { fieldValue } = this.state 80 | const { itemKey, item, isPluginRunning, isEditingFlags } = this.props 81 | const label = item.label || itemKey 82 | const disabled = isPluginRunning || isEditingFlags 83 | let { type } = item 84 | if (!type) type = item.options ? 'select' : 'text' 85 | let options 86 | 87 | switch (type) { 88 | case 'select': 89 | options = item.options.map(el => { 90 | let optionLabel 91 | let optionValue 92 | 93 | if (typeof el === 'string') { 94 | // eg: ['light', 'full', 'fast'] 95 | optionLabel = el 96 | optionValue = el 97 | } else if (typeof el === 'object') { 98 | // eg: [{ label: 'Ropsten (testnet)', value: 'Ropsten', flag: '--testnet' }] 99 | optionLabel = el.label 100 | optionValue = el.value 101 | } else { 102 | throw Error(`el was not properly set: ${el}`) 103 | } 104 | 105 | return { label: optionLabel, value: optionValue } 106 | }) 107 | 108 | return ( 109 |
110 | 161 | this.handleChange(itemKey, event.target.files[0].path) 162 | } 163 | onClick={this.showOpenDialog(itemKey)} 164 | ref={this.inputOpenFileRef} 165 | style={{ display: 'none' }} 166 | webkitdirectory={type.includes('directory') ? 1 : 0} 167 | directory={type.includes('directory') ? 1 : 0} 168 | multiple={type.includes('multiple') ? 1 : 0} 169 | /> 170 |
171 | ) 172 | default: 173 | return ( 174 | this.handleChange(itemKey, event.target.value)} 181 | fullWidth 182 | /> 183 | ) 184 | } 185 | } 186 | } 187 | 188 | function mapStateToProps(state, ownProps) { 189 | const selectedPlugin = state.plugin.selected 190 | 191 | return { 192 | pluginState: state.plugin, 193 | itemValue: state.plugin[selectedPlugin].config[ownProps.itemKey] 194 | } 195 | } 196 | 197 | export default connect(mapStateToProps)(DynamicConfigFormItem) 198 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/DynamicConfigForm/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import Grid from '@material-ui/core/Grid' 5 | import FormItem from './FormItem' 6 | import FlagPreview from './FlagPreview' 7 | import { getGeneratedFlags, setFlags } from '../../../../store/plugin/actions' 8 | 9 | class DynamicConfigForm extends Component { 10 | static propTypes = { 11 | settings: PropTypes.array, 12 | pluginState: PropTypes.object, 13 | plugin: PropTypes.object, 14 | isPluginRunning: PropTypes.bool, 15 | handlePluginConfigChanged: PropTypes.func, 16 | dispatch: PropTypes.func 17 | } 18 | 19 | constructor(props) { 20 | super(props) 21 | 22 | const { pluginState } = props 23 | const preloadPlugin = window.Grid.PluginHost.getPluginByName( 24 | pluginState.selected 25 | ) 26 | const { config, flags } = pluginState[pluginState.selected] 27 | const generatedFlags = getGeneratedFlags(preloadPlugin, config) 28 | const isEditingFlags = !flags.every(f => generatedFlags.includes(f)) 29 | this.state = { isEditingFlags } 30 | } 31 | 32 | toggleEditGeneratedFlags = checked => { 33 | const { pluginState, dispatch } = this.props 34 | const { config } = pluginState[pluginState.selected] 35 | const preloadPlugin = window.Grid.PluginHost.getPluginByName( 36 | pluginState.selected 37 | ) 38 | this.setState({ isEditingFlags: checked }) 39 | if (!checked) { 40 | dispatch(setFlags(preloadPlugin, config)) 41 | } 42 | } 43 | 44 | wrapGridItem = (el, index) => { 45 | return ( 46 | 47 | {el} 48 | 49 | ) 50 | } 51 | 52 | wrapFormItem = item => { 53 | const { plugin, isPluginRunning, handlePluginConfigChanged } = this.props 54 | const { isEditingFlags } = this.state 55 | return ( 56 | 65 | ) 66 | } 67 | 68 | render() { 69 | const { settings, plugin, pluginState, isPluginRunning } = this.props 70 | const { isEditingFlags } = this.state 71 | const { config, flags } = pluginState[pluginState.selected] 72 | 73 | if (!settings) return

No configuration settings found

74 | 75 | const formItems = settings 76 | .filter(setting => !setting.required) // Omit required flags from UI 77 | .map(this.wrapFormItem) 78 | .map(this.wrapGridItem) 79 | 80 | return ( 81 |
82 | 87 | {formItems} 88 | 89 |
90 |
91 | 99 |
100 |
101 | ) 102 | } 103 | } 104 | 105 | function mapStateToProps(state) { 106 | return { 107 | pluginState: state.plugin 108 | } 109 | } 110 | 111 | export default connect(mapStateToProps)(DynamicConfigForm) 112 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/VersionList/LatestVersionWarning.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Button from '@material-ui/core/Button' 4 | import SnackbarContent from '@material-ui/core/SnackbarContent' 5 | import semver from 'semver' 6 | import { withStyles } from '@material-ui/core/styles' 7 | import WarningIcon from '@material-ui/icons/Warning' 8 | import amber from '@material-ui/core/colors/amber' 9 | 10 | const styles = () => ({ 11 | warning: { 12 | backgroundColor: amber[700], 13 | opacity: 0.9, 14 | margin: '10px 0 15px 0' 15 | }, 16 | warningIcon: { 17 | fontSize: 19, 18 | verticalAlign: 'middle', 19 | marginBottom: 2 20 | } 21 | }) 22 | 23 | class LatestVersionWarning extends Component { 24 | static propTypes = { 25 | classes: PropTypes.object, 26 | displayName: PropTypes.string, 27 | latestRelease: PropTypes.object, 28 | selectedVersion: PropTypes.string, 29 | handleReleaseSelect: PropTypes.func.isRequired 30 | } 31 | 32 | static defaultProps = {} 33 | 34 | render() { 35 | const { 36 | classes, 37 | handleReleaseSelect, 38 | latestRelease, 39 | selectedVersion, 40 | displayName 41 | } = this.props 42 | if (!selectedVersion || !latestRelease) return null 43 | const latestVersion = latestRelease.version 44 | 45 | if (semver.compare(selectedVersion, latestVersion)) { 46 | return ( 47 | 51 | You are 52 | using an older version of {displayName} 53 | 54 | } 55 | action={ 56 | 59 | } 60 | /> 61 | ) 62 | } 63 | 64 | return null 65 | } 66 | } 67 | 68 | export default withStyles(styles)(LatestVersionWarning) 69 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/VersionList/VersionListItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import ListItem from '@material-ui/core/ListItem' 4 | import ListItemIcon from '@material-ui/core/ListItemIcon' 5 | import ListItemText from '@material-ui/core/ListItemText' 6 | import Typography from '@material-ui/core/Typography' 7 | import CloudDownloadIcon from '@material-ui/icons/CloudDownload' 8 | import CheckBoxIcon from '@material-ui/icons/CheckBox' 9 | import styled, { css } from 'styled-components' 10 | import Spinner from '../../../shared/Spinner' 11 | import { without } from '../../../../lib/utils' 12 | 13 | export default class VersionListItem extends Component { 14 | static propTypes = { 15 | plugin: PropTypes.object.isRequired, 16 | release: PropTypes.object.isRequired, 17 | handleDownloadError: PropTypes.func.isRequired, 18 | handleReleaseDownloaded: PropTypes.func.isRequired, 19 | handleReleaseSelect: PropTypes.func, 20 | isSelectedRelease: PropTypes.func 21 | } 22 | 23 | state = { 24 | isDownloading: false, 25 | downloadProgress: 0, 26 | extractionProgress: 0, 27 | extractedFile: '', 28 | isHovered: false 29 | } 30 | 31 | componentDidMount() { 32 | // @see https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html 33 | this._isMounted = true 34 | } 35 | 36 | componentWillUnmount() { 37 | this._isMounted = false 38 | } 39 | 40 | releaseDisplayName = release => { 41 | const { plugin } = this.props 42 | const { displayName } = plugin 43 | const { platform, arch, displayVersion } = release 44 | 45 | let metadata 46 | if (!platform) { 47 | metadata = '' 48 | } else { 49 | const platformCapitalized = 50 | platform.charAt(0).toUpperCase() + platform.slice(1) 51 | if (!arch) { 52 | metadata = ` - ${platformCapitalized}` 53 | } else { 54 | metadata = ` - ${platformCapitalized} (${arch})` 55 | } 56 | } 57 | 58 | return `${displayName} ${displayVersion}${metadata}` 59 | } 60 | 61 | downloadRelease = release => { 62 | const { plugin, handleDownloadError, handleReleaseDownloaded } = this.props 63 | const { isDownloading } = this.state 64 | // Return if already downloading 65 | if (isDownloading) return 66 | this.setState({ isDownloading: true }, async () => { 67 | let localRelease 68 | try { 69 | localRelease = await plugin.download( 70 | release, 71 | downloadProgress => { 72 | if (this._isMounted) this.setState({ downloadProgress }) 73 | }, 74 | (extractionProgress, extractedFile) => { 75 | if (this._isMounted) { 76 | this.setState({ extractionProgress, extractedFile }) 77 | } 78 | } 79 | ) 80 | } catch (error) { 81 | handleDownloadError(error) 82 | } 83 | if (this._isMounted) { 84 | this.setState({ 85 | isDownloading: false, 86 | downloadProgress: 0, 87 | extractionProgress: 0, 88 | extractedFile: '' 89 | }) 90 | } 91 | handleReleaseDownloaded(localRelease) 92 | }) 93 | } 94 | 95 | handleReleaseSelect = release => { 96 | const { handleReleaseSelect } = this.props 97 | if (release.remote) { 98 | this.downloadRelease(release) 99 | } else { 100 | handleReleaseSelect(release) 101 | } 102 | } 103 | 104 | renderIcon = release => { 105 | const { isSelectedRelease } = this.props 106 | const { 107 | downloadProgress, 108 | isDownloading, 109 | extractionProgress, 110 | isHovered 111 | } = this.state 112 | let icon = 113 | if (isDownloading) { 114 | icon = ( 115 | 120 | ) 121 | } else if (release.remote) { 122 | icon = 123 | } else if (isSelectedRelease(release)) { 124 | icon = 125 | } else if (!release.remote) { 126 | icon = 127 | } 128 | return icon 129 | } 130 | 131 | toggleHover = () => { 132 | const { isHovered } = this.state 133 | this.setState({ isHovered: !isHovered }) 134 | } 135 | 136 | render() { 137 | const { isSelectedRelease, release } = this.props 138 | const { 139 | downloadProgress, 140 | isDownloading, 141 | extractionProgress, 142 | extractedFile 143 | } = this.state 144 | let actionLabel = 'Use' 145 | if (!release.remote) { 146 | actionLabel = 'Use' 147 | if (isSelectedRelease(release)) { 148 | actionLabel = 'Selected' 149 | } 150 | } else { 151 | actionLabel = 'Download' 152 | if (isDownloading) { 153 | actionLabel = extractionProgress > 0 ? 'Extracting' : 'Downloading' 154 | } 155 | } 156 | 157 | return ( 158 | this.handleReleaseSelect(release)} 163 | selected={isSelectedRelease(release)} 164 | isDownloading={isDownloading} 165 | alt={release.name} 166 | data-test-is-selected={isSelectedRelease(release)} 167 | data-test-is-downloaded={!release.remote} 168 | > 169 | {this.renderIcon(release)} 170 | 0 176 | ? `${extractionProgress}% - ${extractedFile}` 177 | : downloadProgress > 0 178 | ? `${downloadProgress}%` 179 | : null 180 | } 181 | /> 182 | 183 | 187 | {actionLabel} 188 | 189 | 190 | 191 | ) 192 | } 193 | } 194 | 195 | const StyledListItemAction = styled.span` 196 | text-transform: uppercase; 197 | ` 198 | 199 | const ListItemTextVersion = styled(({ isLocalRelease, children, ...rest }) => ( 200 | 206 | {children} 207 | 208 | ))` 209 | ${props => 210 | props.isLocalRelease && 211 | css` 212 | font-weight: bold; 213 | color: grey; 214 | `} 215 | ` 216 | 217 | const HiddenCheckBoxIcon = styled(CheckBoxIcon)` 218 | visibility: hidden; 219 | ` 220 | 221 | const BlankIconPlaceholder = styled.div` 222 | width: 24px; 223 | height: 24px; 224 | ` 225 | 226 | const StyledListItem = styled(without('isDownloading')(ListItem))` 227 | ${props => 228 | !props.selected && 229 | css` 230 | ${StyledListItemAction} { 231 | visibility: hidden; 232 | } 233 | `} 234 | &:hover ${StyledListItemAction} { 235 | visibility: visible; 236 | } 237 | &:hover ${HiddenCheckBoxIcon} { 238 | visibility: visible; 239 | } 240 | ${props => 241 | props.isDownloading && 242 | css` 243 | ${StyledListItemAction} { 244 | visibility: visible; 245 | } 246 | `} 247 | ` 248 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/VersionList/VersionsAvailableText.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import styled from 'styled-components' 4 | import { withStyles } from '@material-ui/core/styles' 5 | import Typography from '@material-ui/core/Typography' 6 | import RefreshIcon from '@material-ui/icons/Refresh' 7 | import Spinner from '../../../shared/Spinner' 8 | 9 | const styles = () => ({ 10 | refreshIcon: { 11 | fontSize: 22, 12 | opacity: 0.5, 13 | marginLeft: 5, 14 | verticalAlign: 'middle', 15 | marginBottom: 4, 16 | visibility: 'hidden' 17 | }, 18 | versionsAvailable: { 19 | '&:hover': { 20 | cursor: 'pointer' 21 | }, 22 | '&:hover $refreshIcon': { 23 | visibility: 'visible' 24 | } 25 | } 26 | }) 27 | 28 | class VersionsAvailableText extends Component { 29 | static displayName = 'VersionsAvailableText' 30 | 31 | static propTypes = { 32 | classes: PropTypes.object, 33 | loadingReleases: PropTypes.bool, 34 | localReleaseCount: PropTypes.number, 35 | totalReleaseCount: PropTypes.number, 36 | lastLoadTimestamp: PropTypes.number, 37 | openCache: PropTypes.func, 38 | loadReleases: PropTypes.func 39 | } 40 | 41 | render() { 42 | const { 43 | classes, 44 | loadingReleases, 45 | localReleaseCount, 46 | totalReleaseCount, 47 | lastLoadTimestamp, 48 | openCache, 49 | loadReleases 50 | } = this.props 51 | 52 | return ( 53 |
54 | {loadingReleases ? ( 55 | 56 | Loading versions... 57 | 58 | 59 | ) : ( 60 | { 63 | loadReleases() 64 | }} 65 | classes={{ root: classes.versionsAvailable }} 66 | data-test-id="button-refresh-version-list" 67 | data-test-timestamp={lastLoadTimestamp} 68 | > 69 | {totalReleaseCount}{' '} 70 | {localReleaseCount === 1 ? 'version' : 'versions'} available 71 | 72 | 73 | )} 74 | 75 | 76 | {})} 78 | style={{ 79 | textDecoration: 'underline', 80 | cursor: 'pointer' 81 | }} 82 | > 83 | {localReleaseCount}{' '} 84 | {localReleaseCount === 1 ? 'release' : 'releases'} downloaded 85 | 86 | 87 |
88 | ) 89 | } 90 | } 91 | 92 | export default withStyles(styles)(VersionsAvailableText) 93 | 94 | const RemoteReleaseLoadingSpinner = styled(Spinner)` 95 | margin-left: 10px; 96 | ` 97 | 98 | const StyledDownloadedVersions = styled.span` 99 | color: lightGrey; 100 | font-size: 13px; 101 | font-weight: bold; 102 | text-transform: uppercase; 103 | ` 104 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginConfig/VersionList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react' 2 | import { connect } from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import styled from 'styled-components' 5 | import List from '@material-ui/core/List' 6 | import Notification from '../../../shared/Notification' 7 | import { setRelease } from '../../../../store/plugin/actions' 8 | import VersionListItem from './VersionListItem' 9 | import VersionsAvailableText from './VersionsAvailableText' 10 | import LatestVersionWarning from './LatestVersionWarning' 11 | import { Grid } from '../../../../API' 12 | 13 | class VersionList extends Component { 14 | static propTypes = { 15 | dispatch: PropTypes.func.isRequired, 16 | handleReleaseSelect: PropTypes.func.isRequired, 17 | plugin: PropTypes.object.isRequired, 18 | selectedRelease: PropTypes.object 19 | } 20 | 21 | state = { 22 | releases: [], 23 | localReleaseCount: 0, 24 | loadingReleases: false, 25 | downloadError: null 26 | } 27 | 28 | componentDidMount() { 29 | this.loadReleases() 30 | } 31 | 32 | componentWillReceiveProps({ plugin: nextPlugin }) { 33 | const { plugin: oldPlugin } = this.props 34 | if (oldPlugin && nextPlugin !== oldPlugin) { 35 | // this.loadLocalReleases() 36 | this.setState({ releases: [] }) 37 | this.loadReleases(nextPlugin) 38 | } 39 | } 40 | 41 | dismissDownloadError = () => { 42 | this.setState({ downloadError: null }) 43 | } 44 | 45 | loadReleases = async plugin => { 46 | // eslint-disable-next-line 47 | plugin = plugin || this.props.plugin 48 | this.setState({ loadingReleases: true }) 49 | const localReleases = {} 50 | // console.time('load releases') 51 | let releases = await plugin.getReleases() 52 | // console.timeEnd('load releases') 53 | // console.time('dedupe') 54 | let count = 0 55 | releases.forEach(r => { 56 | if (!r.remote) { 57 | count += 1 58 | localReleases[r.fileName] = r 59 | } 60 | }) 61 | releases = releases.filter(r => !r.remote || !localReleases[r.fileName]) 62 | // console.timeEnd('dedupe') // for 132 -> 83 ms 63 | this.setState( 64 | { 65 | releases, 66 | loadingReleases: false, 67 | localReleaseCount: count, 68 | // lastLoadTimestamp used in tests 69 | lastLoadTimestamp: new Date().getTime() 70 | }, 71 | () => { 72 | // Set first local release as active if no release is already set 73 | const { selectedRelease } = this.props 74 | if (!selectedRelease) { 75 | const firstLocalRelease = releases.find(release => { 76 | return !release.remote 77 | }) 78 | if (firstLocalRelease) { 79 | this.handleReleaseSelect(firstLocalRelease) 80 | } 81 | } 82 | } 83 | ) 84 | } 85 | 86 | isLocalRelease = release => { 87 | return !release.remote 88 | } 89 | 90 | handleRefresh = () => { 91 | this.loadReleases() 92 | } 93 | 94 | isSelectedRelease = release => { 95 | const { selectedRelease } = this.props 96 | if (!release) return false 97 | return release.fileName === selectedRelease.fileName 98 | } 99 | 100 | handleReleaseSelect = release => { 101 | const { plugin, dispatch, handleReleaseSelect } = this.props 102 | dispatch(setRelease(plugin, release)) 103 | handleReleaseSelect(release) 104 | } 105 | 106 | handleReleaseDownloaded = release => { 107 | const releaseDownloaded = { ...release, remote: false } 108 | const { releases, localReleaseCount } = this.state 109 | const index = releases.findIndex(r => r.fileName === release.fileName) 110 | // releases.splice(index, 0, releaseDownloaded) 111 | releases[index] = releaseDownloaded 112 | this.setState( 113 | { 114 | releases: [...releases], 115 | localReleaseCount: localReleaseCount + 1 116 | }, 117 | () => { 118 | this.handleReleaseSelect(release) 119 | } 120 | ) 121 | } 122 | 123 | renderVersionList = () => { 124 | const { plugin } = this.props 125 | const { releases } = this.state 126 | const renderListItems = () => { 127 | const list = releases.map((release, i) => { 128 | return ( 129 | 136 | this.setState({ downloadError }) 137 | } 138 | handleReleaseDownloaded={this.handleReleaseDownloaded} 139 | /> 140 | ) 141 | }) 142 | return list 143 | } 144 | return ( 145 | 146 | {renderListItems()} 147 | 148 | ) 149 | } 150 | 151 | render() { 152 | const { plugin, selectedRelease } = this.props 153 | const { 154 | downloadError, 155 | loadingReleases, 156 | localReleaseCount, 157 | lastLoadTimestamp, 158 | releases 159 | } = this.state 160 | 161 | return ( 162 | 163 | {downloadError && ( 164 | 169 | )} 170 | 171 | { 177 | this.loadReleases() 178 | }} 179 | openCache={() => { 180 | Grid.openCache(plugin.name) 181 | }} 182 | /> 183 | 184 | 190 | 191 | {this.renderVersionList()} 192 | 193 | ) 194 | } 195 | } 196 | 197 | function mapStateToProps(state) { 198 | return { 199 | selectedRelease: state.plugin[state.plugin.selected].release 200 | } 201 | } 202 | 203 | export default connect(mapStateToProps)(VersionList) 204 | 205 | const StyledList = styled(List)` 206 | min-height: 200px; 207 | max-height: '100%', 208 | overflow-y: scroll; 209 | ` 210 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginsNav.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import { withStyles } from '@material-ui/core/styles' 5 | import Drawer from '@material-ui/core/Drawer' 6 | import List from '@material-ui/core/List' 7 | import ListSubheader from '@material-ui/core/ListSubheader' 8 | import PluginsNavListItem from './PluginsNavListItem' 9 | 10 | const drawerWidth = 240 11 | 12 | const styles = theme => ({ 13 | drawer: { 14 | width: drawerWidth, 15 | flexShrink: 0 16 | }, 17 | drawerPaper: { 18 | width: drawerWidth, 19 | top: 'auto' 20 | }, 21 | content: { 22 | display: 'flex', 23 | flexDirection: 'column', 24 | maxHeight: '100%', 25 | maxWidth: `calc(100% - ${drawerWidth}px)`, 26 | flexGrow: 1, 27 | padding: `${theme.spacing.unit * 4}px ${theme.spacing.unit * 3}px ${theme 28 | .spacing.unit * 3}px` 29 | // marginLeft: -drawerWidth 30 | }, 31 | toolbar: theme.mixins.toolbar, 32 | listSubheader: { 33 | textTransform: 'uppercase', 34 | fontSize: '80%', 35 | height: '40px' 36 | } 37 | }) 38 | 39 | class PluginsNav extends Component { 40 | static propTypes = { 41 | classes: PropTypes.object.isRequired, 42 | plugins: PropTypes.array.isRequired, 43 | pluginState: PropTypes.object.isRequired, 44 | children: PropTypes.node, 45 | handleToggle: PropTypes.func.isRequired, 46 | handleSelectPlugin: PropTypes.func.isRequired, 47 | selectedPluginName: PropTypes.string 48 | } 49 | 50 | isRunning = plugin => { 51 | const { pluginState } = this.props 52 | return [ 53 | 'DOWNLOADING', 54 | 'EXTRACTING', 55 | 'STARTING', 56 | 'STARTED', 57 | 'CONNECTED' 58 | ].includes(pluginState[plugin.name].active.status) 59 | } 60 | 61 | buildListItem = plugin => { 62 | const { 63 | classes, 64 | pluginState, 65 | handleToggle, 66 | handleSelectPlugin, 67 | selectedPluginName 68 | } = this.props 69 | 70 | const { 71 | content, 72 | drawer, 73 | drawerPaper, 74 | toolbar, 75 | listSubheader, 76 | ...restClasses 77 | } = classes 78 | 79 | return ( 80 | 90 | ) 91 | } 92 | 93 | renderLists = () => { 94 | const { plugins, classes } = this.props 95 | const types = [...new Set(plugins.map(plugin => plugin.type))] 96 | const buildList = type => ( 97 | 101 | {type} 102 | 103 | } 104 | > 105 | {this.renderPlugins(type)} 106 | 107 | ) 108 | const render = types.map(type => buildList(type)) 109 | return render 110 | } 111 | 112 | renderPlugins = type => { 113 | const { plugins } = this.props 114 | const renderPlugins = plugins 115 | .filter(plugin => plugin.type === type) 116 | .sort((a, b) => a.order - b.order) 117 | .map(s => this.buildListItem(s)) 118 | return renderPlugins 119 | } 120 | 121 | render() { 122 | const { classes, children } = this.props 123 | const showDrawer = true 124 | return ( 125 | 126 | 132 | {this.renderLists()} 133 | 134 |
{children}
135 |
136 | ) 137 | } 138 | } 139 | 140 | function mapStateToProps(state) { 141 | return { 142 | pluginState: state.plugin, 143 | selectedPluginName: state.plugin.selected 144 | } 145 | } 146 | 147 | export default connect(mapStateToProps)(withStyles(styles)(PluginsNav)) 148 | -------------------------------------------------------------------------------- /src/components/Plugins/PluginsNavListItem.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { withStyles } from '@material-ui/core/styles' 5 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' 6 | import ListItem from '@material-ui/core/ListItem' 7 | import ListItemText from '@material-ui/core/ListItemText' 8 | import Badge from '@material-ui/core/Badge' 9 | import Switch from '@material-ui/core/Switch' 10 | 11 | const styles = () => ({ 12 | pluginName: { 13 | marginRight: 5, 14 | textTransform: 'capitalize', 15 | fontSize: '85%' 16 | }, 17 | hoverableListItem: { 18 | '&:hover $versionInfo': { 19 | visibility: 'visible' 20 | } 21 | }, 22 | versionInfo: { 23 | fontSize: '80%', 24 | visibility: 'hidden' 25 | } 26 | }) 27 | 28 | class PluginsNavListItem extends Component { 29 | static propTypes = { 30 | classes: PropTypes.object.isRequired, 31 | plugin: PropTypes.object.isRequired, 32 | handleToggle: PropTypes.func.isRequired, 33 | handleSelectPlugin: PropTypes.func.isRequired, 34 | isRunning: PropTypes.bool, 35 | isSelected: PropTypes.bool, 36 | secondaryText: PropTypes.string, 37 | appBadges: PropTypes.object 38 | } 39 | 40 | state = { 41 | isToggled: false 42 | } 43 | 44 | badgeContent = () => { 45 | const { appBadges } = this.props 46 | return Object.values(appBadges).reduce((a, b) => a + b, 0) 47 | } 48 | 49 | toggleOff = plugin => newState => { 50 | if (['started', 'connected', 'stopped', 'error'].includes(newState)) { 51 | this.setState({ isToggled: false }) 52 | plugin.off('newState', this.toggleOff) 53 | } 54 | } 55 | 56 | handleSwitch = plugin => { 57 | const { handleToggle } = this.props 58 | this.setState({ isToggled: true }, () => { 59 | plugin.on('newState', this.toggleOff(plugin)) 60 | }) 61 | handleToggle(plugin) 62 | } 63 | 64 | render() { 65 | const { isToggled } = this.state 66 | const { 67 | classes, 68 | handleSelectPlugin, 69 | isRunning, 70 | isSelected, 71 | secondaryText, 72 | plugin 73 | } = this.props 74 | 75 | return ( 76 | handleSelectPlugin(plugin)} 80 | classes={{ 81 | root: classes.hoverableListItem, 82 | selected: classes.selected 83 | }} 84 | button 85 | data-test-id={`node-${plugin.name}`} 86 | > 87 | 90 | {plugin.displayName} 91 | 92 | } 93 | secondary={secondaryText} 94 | primaryTypographyProps={{ 95 | inline: true, 96 | classes: { root: classes.pluginName } 97 | }} 98 | secondaryTypographyProps={{ 99 | inline: true, 100 | classes: { root: classes.versionInfo } 101 | }} 102 | /> 103 | 104 | 105 | this.handleSwitch(plugin)} 108 | checked={isRunning} 109 | disabled={isToggled} 110 | data-test-id={`switch-${plugin.name}`} 111 | /> 112 | 113 | 114 | 115 | ) 116 | } 117 | } 118 | 119 | function mapStateToProps(state, ownProps) { 120 | return { 121 | appBadges: state.plugin[ownProps.plugin.name].appBadges 122 | } 123 | } 124 | 125 | export default connect(mapStateToProps)(withStyles(styles)(PluginsNavListItem)) 126 | -------------------------------------------------------------------------------- /src/components/Plugins/Terminal/TerminalInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withStyles } from '@material-ui/core/styles' 4 | import TextField from '@material-ui/core/TextField' 5 | import InputAdornment from '@material-ui/core/InputAdornment' 6 | import Typography from '@material-ui/core/Typography' 7 | import FormGroup from '@material-ui/core/FormGroup' 8 | import FormControlLabel from '@material-ui/core/FormControlLabel' 9 | import Checkbox from '@material-ui/core/Checkbox' 10 | 11 | const styles = () => ({ 12 | textField: { 13 | paddingRight: '2px' 14 | }, 15 | inputAdornment: { 16 | color: '#eee', 17 | paddingLeft: '2px' 18 | }, 19 | input: { 20 | background: '#111', 21 | color: '#eee', 22 | fontFamily: 23 | 'Lucida Console, Lucida Sans Typewriter, monaco, Bitstream Vera Sans Mono, monospace', 24 | fontSize: '11px', 25 | overflowX: 'auto', 26 | padding: '5px' 27 | } 28 | }) 29 | 30 | class TerminalInput extends Component { 31 | static propTypes = { 32 | plugin: PropTypes.object.isRequired, 33 | addNewLog: PropTypes.func, 34 | classes: PropTypes.object 35 | } 36 | 37 | constructor(props) { 38 | super(props) 39 | this.state = { 40 | input: '', 41 | history: [''], 42 | historyIndex: 0, 43 | protectInput: false 44 | } 45 | } 46 | 47 | toggleProtect = event => { 48 | this.setState({ protectInput: event.target.checked }) 49 | } 50 | 51 | handleChange = event => { 52 | this.setState({ input: event.target.value }) 53 | } 54 | 55 | handleKeyDown = event => { 56 | // On up or down arrow, navigate through history 57 | if (event.keyCode === 38 || event.keyCode === 40) { 58 | event.preventDefault() 59 | const { history, historyIndex } = this.state 60 | let newIndex = historyIndex 61 | if (event.keyCode === 38) { 62 | // up arrow 63 | newIndex += 1 64 | if (newIndex > history.length) { 65 | newIndex = 0 66 | } 67 | } else if (event.keyCode === 40) { 68 | // down arrow 69 | newIndex -= 1 70 | if (newIndex < 0) { 71 | newIndex = history.length 72 | } 73 | } 74 | const input = history[history.length - newIndex] 75 | this.setState({ input, historyIndex: newIndex }) 76 | } 77 | } 78 | 79 | submit = () => { 80 | const { plugin, addNewLog } = this.props 81 | const { input, history, protectInput } = this.state 82 | plugin.write(input) 83 | addNewLog(protectInput ? '***' : input) 84 | this.setState({ input: '', historyIndex: 0 }) 85 | // Add to history if not same as last history entry 86 | if (history[-1] !== input) { 87 | const newHistory = [...history, input] 88 | this.setState({ history: newHistory }) 89 | } 90 | } 91 | 92 | render() { 93 | const { classes, plugin } = this.props 94 | const { input, protectInput } = this.state 95 | 96 | const isPluginRunning = ['STARTED', 'CONNECTED'].includes(plugin.state) 97 | 98 | if (!isPluginRunning) { 99 | return null 100 | } 101 | 102 | return ( 103 |
104 | 105 | 120 | 121 | > 122 | 123 | 124 | ) 125 | }} 126 | fullWidth 127 | /> 128 | 136 | } 137 | label="Hide Input" 138 | /> 139 | 140 |
141 | ) 142 | } 143 | } 144 | 145 | export default withStyles(styles)(TerminalInput) 146 | -------------------------------------------------------------------------------- /src/components/Plugins/Terminal/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Ansi from 'ansi-to-react' 4 | import { withStyles } from '@material-ui/core/styles' 5 | import TerminalInput from './TerminalInput' 6 | 7 | const styles = () => ({ 8 | terminalWrapper: { 9 | background: '#111', 10 | color: '#eee', 11 | fontFamily: 12 | 'Lucida Console, Lucida Sans Typewriter, monaco, Bitstream Vera Sans Mono, monospace', 13 | fontSize: '11px', 14 | padding: 10, 15 | 16 | // Fluid width and height with support to scrolling 17 | maxWidth: '100%', 18 | height: 'calc(100vh - 330px)', 19 | 20 | // Scroll config 21 | overflowX: 'auto', 22 | overflowY: 'auto', 23 | whiteSpace: 'nowrap' 24 | } 25 | }) 26 | 27 | class Terminal extends Component { 28 | static propTypes = { 29 | plugin: PropTypes.object.isRequired, 30 | classes: PropTypes.object 31 | } 32 | 33 | constructor(props) { 34 | super(props) 35 | this.state = { 36 | logs: [] 37 | } 38 | this.terminalScrollViewRef = React.createRef() 39 | } 40 | 41 | componentDidMount = async () => { 42 | this.subscribeLogs() 43 | } 44 | 45 | componentWillReceiveProps({ plugin: nextPlugin }) { 46 | const { plugin: oldPlugin } = this.props 47 | const logs = nextPlugin.getLogs() 48 | this.setState({ logs }) 49 | if (oldPlugin && nextPlugin !== oldPlugin) { 50 | this.unsubscribeLogs(oldPlugin) 51 | this.subscribeLogs(nextPlugin) 52 | } 53 | } 54 | 55 | componentDidUpdate = () => { 56 | this.terminalScrollToBottom() 57 | } 58 | 59 | componentWillUnmount() { 60 | this.unsubscribeLogs() 61 | } 62 | 63 | addNewLog = async newLog => { 64 | const { logs } = this.state 65 | this.setState({ 66 | logs: [...logs, newLog] 67 | }) 68 | } 69 | 70 | clearLogs = newState => { 71 | if (newState === 'started') { 72 | this.setState({ logs: [] }) 73 | } 74 | } 75 | 76 | subscribeLogs = plugin => { 77 | // eslint-disable-next-line 78 | plugin = plugin || this.props.plugin 79 | plugin.on('log', this.addNewLog) 80 | // Clear old logs on restart 81 | plugin.on('newState', this.clearLogs) 82 | } 83 | 84 | unsubscribeLogs = plugin => { 85 | // eslint-disable-next-line 86 | plugin = plugin || this.props.plugin 87 | plugin.removeListener('log', this.addNewLog) 88 | plugin.removeListener('newState', this.clearLogs) 89 | } 90 | 91 | terminalScrollToBottom = () => { 92 | const scrollView = this.terminalScrollViewRef.current 93 | if (!scrollView) { 94 | return 95 | } 96 | const { scrollHeight } = scrollView 97 | scrollView.scrollTo({ 98 | top: scrollHeight, 99 | behavior: 'smooth' 100 | }) 101 | } 102 | 103 | render() { 104 | const { classes, plugin } = this.props 105 | const { logs } = this.state 106 | 107 | if (logs.length === 0) { 108 | return
No logs yet.
109 | } 110 | 111 | const renderLogs = logs.map((l, index) => ( 112 |
113 | {' '} 114 | > {l} 115 |
116 | )) 117 | 118 | return ( 119 |
120 |
125 | {renderLogs} 126 |
127 | 128 |
129 | ) 130 | } 131 | } 132 | 133 | export default withStyles(styles)(Terminal) 134 | -------------------------------------------------------------------------------- /src/components/Plugins/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import PropTypes from 'prop-types' 4 | import PluginConfig from './PluginConfig' 5 | import PluginsNav from './PluginsNav' 6 | import { 7 | initPlugin, 8 | selectPlugin, 9 | setConfig, 10 | togglePlugin 11 | } from '../../store/plugin/actions' 12 | import { 13 | getPersistedPluginSelection, 14 | getPersistedTabSelection 15 | } from '../../lib/utils' 16 | 17 | import Grid from '../../API/Grid' 18 | 19 | const { PluginHost } = Grid 20 | 21 | class PluginsTab extends Component { 22 | static propTypes = { 23 | pluginState: PropTypes.object.isRequired, 24 | dispatch: PropTypes.func.isRequired 25 | } 26 | 27 | state = { 28 | plugins: [], 29 | selectedPlugin: undefined 30 | } 31 | 32 | componentDidMount() { 33 | if (!PluginHost) return 34 | const plugins = PluginHost.getAllPlugins() 35 | this.initPlugins(plugins) 36 | } 37 | 38 | initPlugins = plugins => { 39 | const { pluginState, dispatch } = this.props 40 | 41 | // Sync plugins with Redux 42 | plugins.map(plugin => dispatch(initPlugin(plugin))) 43 | 44 | // Set the selected plugin from config.json or a fallback method 45 | const selectedPlugin = 46 | plugins.find(plugin => plugin.name === getPersistedPluginSelection()) || 47 | plugins.find(plugin => plugin.name === pluginState.selected) || 48 | plugins.find(plugin => plugin.order === 1) || 49 | plugins[0] 50 | const selectedTab = getPersistedTabSelection() 51 | this.handleSelectPlugin(selectedPlugin, selectedTab) 52 | 53 | // TODO: two sources of truth - local and redux state 54 | this.setState({ plugins }) 55 | } 56 | 57 | isDisabled = plugin => { 58 | const { selectedRelease } = plugin 59 | return !selectedRelease 60 | } 61 | 62 | handleSelectPlugin = (plugin, tab) => { 63 | const { dispatch } = this.props 64 | 65 | this.setState({ selectedPlugin: plugin }, () => { 66 | dispatch(selectPlugin(plugin.name, tab)) 67 | }) 68 | } 69 | 70 | handlePluginConfigChanged = (key, value) => { 71 | const { pluginState, dispatch } = this.props 72 | const { plugins } = this.state 73 | 74 | const activePlugin = plugins.filter(p => p.name === pluginState.selected)[0] 75 | 76 | const { config } = pluginState[pluginState.selected] 77 | const newConfig = { ...config } 78 | newConfig[key] = value 79 | 80 | dispatch(setConfig(activePlugin, newConfig)) 81 | } 82 | 83 | handleReleaseSelect = release => { 84 | const { selectedPlugin } = this.state 85 | selectedPlugin.selectedRelease = release 86 | this.setState({ selectedPlugin, selectedRelease: release }) 87 | } 88 | 89 | handleToggle = plugin => { 90 | const { pluginState, dispatch } = this.props 91 | // TODO: refactor to only require pluginName to toggle, 92 | // then function can be placed in PlguinsNavListItem.js 93 | // instead of needing to be passed through props. 94 | dispatch(togglePlugin(plugin, pluginState[plugin.name].release)) 95 | } 96 | 97 | render() { 98 | const { plugins, selectedPlugin, selectedRelease } = this.state 99 | 100 | return ( 101 | 106 | {selectedPlugin && ( 107 | 113 | )} 114 | 115 | ) 116 | } 117 | } 118 | 119 | function mapStateToProps(state) { 120 | return { 121 | pluginState: state.plugin 122 | } 123 | } 124 | 125 | export default connect(mapStateToProps)(PluginsTab) 126 | -------------------------------------------------------------------------------- /src/components/Webview/UrlBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { withStyles } from '@material-ui/core/styles' 4 | import Paper from '@material-ui/core/Paper' 5 | import InputBase from '@material-ui/core/InputBase' 6 | import Divider from '@material-ui/core/Divider' 7 | import IconButton from '@material-ui/core/IconButton' 8 | import ArrowIcon from '@material-ui/icons/ArrowForward' 9 | import BuildIcon from '@material-ui/icons/Build' 10 | 11 | const styles = { 12 | root: { 13 | padding: '2px 4px', 14 | display: 'flex', 15 | alignItems: 'center' 16 | }, 17 | input: { 18 | marginLeft: 8, 19 | flex: 1 20 | }, 21 | iconButton: { 22 | padding: 10 23 | }, 24 | divider: { 25 | width: 1, 26 | height: 28, 27 | margin: 4 28 | } 29 | } 30 | 31 | class CustomizedInputBase extends React.Component { 32 | static propTypes = { 33 | onOpenDevTools: PropTypes.func.isRequired, 34 | onNavigate: PropTypes.func.isRequired 35 | } 36 | 37 | state = { 38 | currentUrl: 'http://localhost:3000/' 39 | } 40 | 41 | handleUrlChange = event => { 42 | this.setState({ currentUrl: event.target.value }) 43 | } 44 | 45 | handleKeyDown = e => { 46 | const { currentUrl } = this.state 47 | const { onNavigate } = this.props 48 | if (e.key === 'Enter') { 49 | onNavigate(currentUrl) 50 | } 51 | } 52 | 53 | navigate = newUrl => { 54 | const { onNavigate } = this.props 55 | this.setState({ 56 | currentUrl: newUrl 57 | }) 58 | onNavigate(newUrl) 59 | } 60 | 61 | render() { 62 | const { classes, onNavigate, onOpenDevTools } = this.props 63 | const { currentUrl } = this.state 64 | return ( 65 | 66 | 74 | 75 | onNavigate(currentUrl)} 80 | > 81 | 82 | 83 | 84 | {/* 85 | 93 | */} 94 | 95 | onOpenDevTools()}> 96 | 97 | 98 | 99 | ) 100 | } 101 | } 102 | 103 | CustomizedInputBase.propTypes = { 104 | classes: PropTypes.object.isRequired 105 | } 106 | 107 | export default withStyles(styles)(CustomizedInputBase) 108 | -------------------------------------------------------------------------------- /src/components/Webview/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import UrlBar from './UrlBar' 4 | 5 | class Webview extends React.Component { 6 | static propTypes = { 7 | url: PropTypes.string 8 | } 9 | 10 | state = { 11 | currentUrl: 'http://localhost:3000', 12 | showUrlBar: true 13 | } 14 | 15 | componentDidMount() { 16 | const { url } = this.props 17 | const { currentUrl } = this.state 18 | const { webview } = this 19 | webview.addEventListener('will-navigate', this.handleWillNavigate) 20 | webview.addEventListener('ipc-message', this.handleIpcMessage) 21 | webview.addEventListener('dom-ready', this.handleDomReady) 22 | webview.addEventListener('did-fail-load', this.handleDidFailLoad) 23 | if (url) { 24 | this.setState({ 25 | currentUrl: url, 26 | // if url is localhost:3000 -> "dev app" -> show url bar 27 | showUrlBar: url === currentUrl 28 | }) 29 | } 30 | } 31 | 32 | componentWillUnmount() { 33 | const { webview } = this 34 | webview.removeEventListener('will-navigate', this.handleNavigate) 35 | webview.removeEventListener('ipc-message', this.handleIpcMessage) 36 | webview.removeEventListener('dom-ready', this.handleDomReady) 37 | webview.removeEventListener('did-fail-load', this.handleDidFailLoad) 38 | } 39 | 40 | handleDomReady = () => { 41 | console.log('Webview: DOM ready') 42 | } 43 | 44 | handleWillNavigate = () => { 45 | console.log('Webview: will navigate') 46 | } 47 | 48 | handleIpcMessage = () => { 49 | console.log('Webview: handle ipc') 50 | } 51 | 52 | handleDidFailLoad = error => { 53 | const { webview } = this 54 | if (error.errorCode === -102) { 55 | // ERR_CONNECTION_REFUSED 56 | webview.loadURL(`${document.URL}/errors/404.html`) 57 | } 58 | } 59 | 60 | handleNavigate = newUrl => { 61 | const { currentUrl } = this.state 62 | if (currentUrl === newUrl) { 63 | const { webview } = this 64 | webview.reload() 65 | } 66 | // TODO prevent non-localhost http navigation 67 | this.setState({ 68 | currentUrl: newUrl 69 | }) 70 | } 71 | 72 | openDevTools = () => { 73 | const { webview } = this 74 | webview.openDevTools() 75 | } 76 | 77 | render() { 78 | const { currentUrl, showUrlBar } = this.state 79 | return ( 80 |
81 | {showUrlBar && ( 82 | 86 | )} 87 |
93 | { 95 | this.webview = ref 96 | }} 97 | src={currentUrl} 98 | style={{ 99 | width: '100%', 100 | height: '100%' 101 | }} 102 | /> 103 |
104 |
105 | ) 106 | } 107 | } 108 | 109 | export default Webview 110 | -------------------------------------------------------------------------------- /src/components/Window.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Window, TitleBar } from 'react-desktop/windows' 3 | import { Grid } from '../API' 4 | 5 | class ConditionalWindow extends Component { 6 | state = { 7 | isMaximized: false 8 | } 9 | 10 | renderWithWindow = children => { 11 | const { isMaximized } = this.state 12 | return ( 13 | 14 | { 20 | Grid.window.close() 21 | }} 22 | onMaximizeClick={() => { 23 | Grid.window.maximize() 24 | this.setState({ isMaximized: true }) 25 | }} 26 | onMinimizeClick={() => { 27 | Grid.window.minimize() 28 | }} 29 | onRestoreDownClick={() => { 30 | Grid.window.unmaximize() 31 | this.setState({ isMaximized: false }) 32 | }} 33 | /> 34 |
41 | {children} 42 |
43 |
44 | ) 45 | } 46 | 47 | render() { 48 | const platform = Grid.platform && Grid.platform.name 49 | // eslint-disable-next-line react/prop-types 50 | const { children } = this.props 51 | const isWindows = platform === 'win32' 52 | return isWindows && Grid.window && !Grid.window.hasFrame() ? ( 53 | this.renderWithWindow(children) 54 | ) : ( 55 |
62 | {children} 63 |
64 | ) 65 | } 66 | } 67 | 68 | export default ConditionalWindow 69 | -------------------------------------------------------------------------------- /src/components/shared/Button.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled, { css } from 'styled-components' 3 | import PropTypes from 'prop-types' 4 | import MuiButton from '@material-ui/core/Button' 5 | import { primary, primary2 } from '../../theme' 6 | 7 | const StyledButton = styled(MuiButton)` 8 | ${props => 9 | props.color === 'primary' && 10 | css` 11 | background-color: ${primary}; 12 | background-image: linear-gradient(45deg, ${primary2} 0%, ${primary} 100%); 13 | `} 14 | ` 15 | 16 | export default class Button extends Component { 17 | static displayName = 'Button' 18 | 19 | static propTypes = { 20 | children: PropTypes.node.isRequired, 21 | color: PropTypes.oneOf(['primary', 'secondary']), 22 | disabled: PropTypes.bool, 23 | onClick: PropTypes.func.isRequired, 24 | secondary: PropTypes.bool, 25 | type: PropTypes.oneOf(['button', 'reset', 'submit']), 26 | className: PropTypes.string, 27 | variant: PropTypes.oneOf([ 28 | 'text', 29 | 'outlined', 30 | 'contained', 31 | 'fab', 32 | 'extendedFab', 33 | 'flat', 34 | 'raised' 35 | ]) 36 | } 37 | 38 | static defaultProps = { 39 | color: 'primary', 40 | disabled: false, 41 | type: 'button', 42 | variant: 'contained' 43 | } 44 | 45 | render() { 46 | const { children } = this.props 47 | 48 | return {children} 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/shared/HelpFab.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Fab from '@material-ui/core/Fab' 3 | import styled from 'styled-components' 4 | import FeedbackIcon from '@material-ui/icons/Feedback' 5 | import { primary, primary2, primary3 } from '../../theme' 6 | 7 | const StyledButton = styled(Fab)` 8 | background-color: #fad961; 9 | background-image: linear-gradient( 10 | 45deg, 11 | ${primary3} 0%, 12 | ${primary2} 50%, 13 | ${primary} 100% 14 | ); 15 | ` 16 | 17 | class HelpFab extends React.Component { 18 | handleButtonClick = () => { 19 | // Feedback form 20 | window.location.href = 'https://forms.gle/bjkphVS8ca1JzwL46' 21 | } 22 | 23 | render() { 24 | return ( 25 | 36 | 37 | 38 | ) 39 | } 40 | } 41 | export default HelpFab 42 | -------------------------------------------------------------------------------- /src/components/shared/Notification.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import { withStyles } from '@material-ui/core/styles' 5 | import Snackbar from '@material-ui/core/Snackbar' 6 | import SnackbarContent from '@material-ui/core/SnackbarContent' 7 | import IconButton from '@material-ui/core/IconButton' 8 | import ErrorIcon from '@material-ui/icons/Error' 9 | import InfoIcon from '@material-ui/icons/Info' 10 | import CheckCircleIcon from '@material-ui/icons/CheckCircle' 11 | import WarningIcon from '@material-ui/icons/Warning' 12 | import CloseIcon from '@material-ui/icons/Close' 13 | import green from '@material-ui/core/colors/green' 14 | import amber from '@material-ui/core/colors/amber' 15 | 16 | const styles = theme => ({ 17 | success: { 18 | backgroundColor: green[600] 19 | }, 20 | error: { 21 | backgroundColor: theme.palette.error.dark 22 | }, 23 | info: { 24 | backgroundColor: theme.palette.primary.dark 25 | }, 26 | warning: { 27 | backgroundColor: amber[700] 28 | }, 29 | icon: { 30 | fontSize: 20, 31 | opacity: 0.9, 32 | verticalAlign: 'middle', 33 | marginBottom: 1 34 | }, 35 | inlineIcon: { 36 | marginRight: theme.spacing.unit 37 | }, 38 | closeIcon: { 39 | opacity: 0.9 40 | } 41 | }) 42 | 43 | class Notification extends Component { 44 | static propTypes = { 45 | classes: PropTypes.object.isRequired, 46 | type: PropTypes.string, 47 | message: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 48 | onDismiss: PropTypes.func.isRequired 49 | } 50 | 51 | render() { 52 | const { classes, message, type, onDismiss } = this.props 53 | const inlineIconClasses = { 54 | classes: { 55 | root: classNames(classes.icon, classes.inlineIcon) 56 | } 57 | } 58 | let icon 59 | let snackbarClasses 60 | 61 | switch (type) { 62 | case 'error': 63 | snackbarClasses = classes.error 64 | icon = 65 | break 66 | case 'warning': 67 | snackbarClasses = classes.warning 68 | icon = 69 | break 70 | case 'info': 71 | snackbarClasses = classes.info 72 | icon = 73 | break 74 | case 'success': 75 | snackbarClasses = classes.success 76 | icon = 77 | break 78 | default: 79 | break 80 | } 81 | 82 | const displayMessage = 83 | typeof message === 'object' ? message.message : message 84 | 85 | return ( 86 | 96 | 100 | {icon} 101 | {displayMessage} 102 | 103 | } 104 | action={[ 105 | 111 | 114 | 115 | ]} 116 | /> 117 | 118 | ) 119 | } 120 | } 121 | export default withStyles(styles)(Notification) 122 | -------------------------------------------------------------------------------- /src/components/shared/Select.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import PropTypes from 'prop-types' 4 | import FormControl from '@material-ui/core/FormControl' 5 | import MuiSelect from '@material-ui/core/Select' 6 | import OutlinedInput from '@material-ui/core/OutlinedInput' 7 | import InputLabel from '@material-ui/core/InputLabel' 8 | import MenuItem from '@material-ui/core/MenuItem' 9 | 10 | export default class Select extends Component { 11 | static displayName = 'Select' 12 | 13 | static propTypes = { 14 | id: PropTypes.string, 15 | name: PropTypes.string, 16 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 17 | onChange: PropTypes.func, 18 | options: PropTypes.array, 19 | disabled: PropTypes.bool 20 | } 21 | 22 | static defaultProps = { 23 | options: [], 24 | disabled: false 25 | } 26 | 27 | constructor(props) { 28 | super(props) 29 | this.state = { labelWidth: 0 } 30 | } 31 | 32 | componentDidMount() { 33 | this.setState({ 34 | // eslint-disable-next-line 35 | labelWidth: ReactDOM.findDOMNode(this.InputLabelRef).offsetWidth 36 | }) 37 | } 38 | 39 | render() { 40 | const { name, id, onChange, options, disabled, value } = this.props 41 | const { labelWidth } = this.state 42 | 43 | const opts = options.map(option => ( 44 | 45 | {option.label} 46 | 47 | )) 48 | 49 | return ( 50 | 55 | { 57 | this.InputLabelRef = ref 58 | }} 59 | htmlFor={id} 60 | > 61 | {name} 62 | 63 | onChange(e.target.value)} 66 | input={} 67 | > 68 | {opts} 69 | 70 | 71 | ) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/shared/Spinner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import CircularProgress from '@material-ui/core/CircularProgress' 4 | 5 | export default class Spinner extends Component { 6 | static displayName = 'Spinner' 7 | 8 | static propTypes = { 9 | color: PropTypes.oneOf(['primary', 'secondary']), 10 | size: PropTypes.number 11 | } 12 | 13 | static defaultProps = { 14 | color: 'primary', 15 | size: 40 16 | } 17 | 18 | render() { 19 | return 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/icons/browse-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/grid-ui/e686c30738e6e1707b513233d9a59597eb06f652/src/icons/browse-icon@2x.png -------------------------------------------------------------------------------- /src/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/grid-ui/e686c30738e6e1707b513233d9a59597eb06f652/src/icons/icon.png -------------------------------------------------------------------------------- /src/icons/icon2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethereum/grid-ui/e686c30738e6e1707b513233d9a59597eb06f652/src/icons/icon2x.png -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | height: 100%; 7 | padding: 0; 8 | margin: 0; 9 | overflow: hidden; 10 | } 11 | 12 | #root { 13 | height: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import './scrollbar-fix.css' 5 | import { Provider } from 'react-redux' 6 | import App from './components/App' 7 | import { Grid } from './API' 8 | import configureStore from './store' 9 | import Webview from './components/Webview' 10 | import Apps from './components/Apps' 11 | import Window from './components/Window' 12 | 13 | const store = configureStore() 14 | const root = document.getElementById('root') 15 | const args = (Grid && Grid.window && Grid.window.getArgs()) || {} 16 | 17 | let themeMode = 'dark' 18 | if (args.scope && args.scope.themeMode === 'light') { 19 | themeMode = 'light' 20 | } 21 | 22 | if (args.isApp) { 23 | ReactDOM.render( 24 | 25 | 26 | 27 | 28 | , 29 | root 30 | ) 31 | } else if (args.scope && args.scope.component === 'apps') { 32 | ReactDOM.render( 33 | 34 | 35 |
39 | 40 |
41 |
42 |
, 43 | root 44 | ) 45 | } else { 46 | ReactDOM.render( 47 | 48 | 49 | 50 | 51 | , 52 | root 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/flags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This method: 3 | * 1) returns nothing if ignoreIfEmpty && value === '' 4 | * 2) preserves spaces contained in the value. 5 | * input: "--ipc '%s'", "/path with spaces" 6 | * output: ["--ipc", "/path with spaces"] 7 | */ 8 | const parseFlag = (pattern, value, ignoreIfEmpty) => { 9 | if (ignoreIfEmpty && !value) { 10 | return '' 11 | } 12 | const result = pattern.split(' ').map(e => e.replace(/%s/, value)) 13 | return result 14 | } 15 | 16 | export const generateFlags = (userConfig, nodeSettings) => { 17 | if (!Array.isArray(nodeSettings)) 18 | throw new Error('Settings must be an Array instance') 19 | 20 | const userConfigEntries = Object.keys(userConfig) 21 | let flags = [] 22 | 23 | userConfigEntries.forEach(entry => { 24 | let pattern 25 | let configEntry = nodeSettings.find(setting => setting.id === entry) 26 | let flagString = configEntry.flag 27 | 28 | if (flagString) { 29 | pattern = flagString 30 | } else if (configEntry.options) { 31 | const options = configEntry.options 32 | const selectedOption = options.find( 33 | option => 34 | userConfig[entry] === option.value || userConfig[entry] === option 35 | ) 36 | if (typeof selectedOption['flag'] !== 'string') { 37 | throw new Error( 38 | `Option "${selectedOption.value || 39 | selectedOption}" must have the "flag" key` 40 | ) 41 | } 42 | pattern = selectedOption.flag 43 | } else { 44 | throw new Error(`Config entry "${entry}" must have the "flag" key`) 45 | } 46 | 47 | const parsedFlag = parseFlag( 48 | pattern, 49 | userConfig[entry], 50 | configEntry.ignoreIfEmpty 51 | ) 52 | flags = flags.concat(parsedFlag) 53 | }) 54 | 55 | return flags.filter(flag => flag.length > 0) 56 | } 57 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Grid } from '../API' 3 | import { generateFlags } from './flags' 4 | 5 | // calling styled(without('unneededProp')(TheComponent)) 6 | // helps satisfy error of extra StyledComponents props passing into children 7 | // see: https://github.com/styled-components/styled-components/pull/2093#issuecomment-461510706 8 | export const without = (...omitProps) => { 9 | const omitSingle = (object = {}, key) => { 10 | if (key === null || key === undefined || !(key in object)) return object 11 | const { [key]: deleted, ...otherKeys } = object 12 | return otherKeys 13 | } 14 | 15 | const omit = (object = {}, keys) => { 16 | if (!keys) return object 17 | if (Array.isArray(keys)) { 18 | // calling omitMultiple here would result in a second array check 19 | return keys.reduce((result, key) => { 20 | if (key in result) { 21 | return omitSingle(result, key) 22 | } 23 | return result 24 | }, object) 25 | } 26 | return omitSingle(object, keys) 27 | } 28 | // HoF 29 | return C => { 30 | const WithoutPropsComponent = ({ children, ...props }) => { 31 | return React.createElement(C, omit(props, omitProps), children) 32 | } 33 | return WithoutPropsComponent 34 | } 35 | } 36 | 37 | export const getPluginSettingsConfig = plugin => { 38 | try { 39 | const settings = plugin.plugin.config.settings // eslint-disable-line 40 | return Array.isArray(settings) ? settings : [] 41 | } catch (e) { 42 | return [] 43 | } 44 | } 45 | 46 | export const getDefaultSetting = (plugin, id) => { 47 | try { 48 | const setting = plugin.plugin.config.settings.find( 49 | setting => setting.id === id 50 | ) 51 | return setting.default 52 | } catch (e) { 53 | return '' 54 | } 55 | } 56 | 57 | export const getDefaultFlags = (plugin, config) => { 58 | const pluginDefaults = {} 59 | const pluginSettings = getPluginSettingsConfig(plugin) 60 | pluginSettings.forEach(setting => { 61 | if ('default' in setting) { 62 | pluginDefaults[setting.id] = setting.default 63 | } 64 | }) 65 | 66 | return generateFlags(pluginDefaults, pluginSettings) 67 | } 68 | 69 | export const getSettingsIds = plugin => { 70 | try { 71 | return plugin.plugin.config.settings.map(setting => setting.id) 72 | } catch (e) { 73 | return [] 74 | } 75 | } 76 | 77 | export const getPersistedPluginSettings = pluginName => { 78 | try { 79 | const settings = Grid.Config.getItem('settings') 80 | return settings[pluginName] || {} 81 | } catch (e) { 82 | return {} 83 | } 84 | } 85 | 86 | export const getPersistedFlags = pluginName => { 87 | try { 88 | const flags = Grid.Config.getItem('flags') 89 | return flags[pluginName] || null 90 | } catch (e) { 91 | return null 92 | } 93 | } 94 | 95 | export const getPersistedPluginSelection = () => { 96 | try { 97 | const settings = Grid.Config.getItem('settings') 98 | return settings.selected || '' 99 | } catch (e) { 100 | return '' 101 | } 102 | } 103 | 104 | export const getPersistedTabSelection = () => { 105 | try { 106 | const settings = Grid.Config.getItem('settings') 107 | return settings.selectedTab || 0 108 | } catch (e) { 109 | return 0 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/scrollbar-fix.css: -------------------------------------------------------------------------------- 1 | /* Base properties */ 2 | .scroll-container::-webkit-scrollbar { 3 | width: 4px; 4 | border-radius: 2px; 5 | } 6 | .scroll-container::-webkit-scrollbar-thumb { 7 | border-radius: 2px; 8 | transition: 200ms all ease-out; 9 | } 10 | 11 | /* Scrollbar track and thumb starts transparent */ 12 | .scroll-container::-webkit-scrollbar { 13 | background: rgba(1, 1, 1, 0.1); 14 | } 15 | .scroll-container::-webkit-scrollbar-thumb { 16 | background: rgba(255, 255, 255, 0.1); 17 | } 18 | 19 | /* Shows scrollbar and thumb when hovering the window */ 20 | html:hover .scroll-container::-webkit-scrollbar { 21 | background: rgba(1, 1, 1, 0.2); 22 | } 23 | html:hover .scroll-container::-webkit-scrollbar-thumb { 24 | /* 30% white */ 25 | /* background: #4c4c4c; */ 26 | background: linear-gradient(#585858, #424242); 27 | } 28 | 29 | html:hover .scroll-container::-webkit-scrollbar-thumb:hover { 30 | background: #727272; 31 | } 32 | html:hover .scroll-container::-webkit-scrollbar-thumb:active { 33 | background: #a7a7a7; 34 | } 35 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import { composeWithDevTools } from 'remote-redux-devtools' 3 | import thunk from 'redux-thunk' 4 | import { saveSettings } from './middleware' 5 | import rootReducer from './rootReducer' 6 | 7 | // In development, send Redux actions to a local DevTools server 8 | // Note: run and view these DevTools with `yarn dev:tools` 9 | let debugWrapper = data => data 10 | if (process.env.NODE_ENV === 'development') { 11 | debugWrapper = composeWithDevTools({ 12 | realtime: true, 13 | port: 8000, 14 | maxAge: 100 15 | }) 16 | } 17 | 18 | export default function configureStore() { 19 | const store = createStore( 20 | rootReducer, 21 | debugWrapper(applyMiddleware(thunk, saveSettings)) 22 | ) 23 | 24 | if (module.hot) { 25 | module.hot.accept('./rootReducer', () => { 26 | const nextPersistedReducer = require('./rootReducer').default // eslint-disable-line 27 | store.replaceReducer(nextPersistedReducer) 28 | }) 29 | } 30 | 31 | return store 32 | } 33 | -------------------------------------------------------------------------------- /src/store/middleware.js: -------------------------------------------------------------------------------- 1 | import { Grid } from '../API' 2 | 3 | // eslint-disable-next-line 4 | export const saveSettings = store => next => async action => { 5 | if (action.type === 'PLUGIN:SET_CONFIG') { 6 | const settings = Grid.Config.getItem('settings') 7 | const newSettings = Object.assign({}, settings) 8 | newSettings[action.payload.pluginName] = action.payload.config 9 | Grid.Config.setItem('settings', newSettings) 10 | } 11 | 12 | if (action.type === 'PLUGIN:SET_FLAGS') { 13 | const flags = Grid.Config.getItem('flags') 14 | const newFlags = Object.assign({}, flags) 15 | newFlags[action.payload.pluginName] = action.payload.flags 16 | Grid.Config.setItem('flags', newFlags) 17 | } 18 | 19 | if (action.type === 'PLUGIN:SELECT') { 20 | const settings = Grid.Config.getItem('settings') 21 | const newSettings = Object.assign({}, settings) 22 | newSettings.selected = action.payload.pluginName 23 | Grid.Config.setItem('settings', newSettings) 24 | } 25 | 26 | if (action.type === 'PLUGIN:SELECT_TAB') { 27 | const settings = Grid.Config.getItem('settings') 28 | const newSettings = Object.assign({}, settings, { 29 | selectedTab: action.payload.tab 30 | }) 31 | Grid.Config.setItem('settings', newSettings) 32 | } 33 | 34 | return next(action) 35 | } 36 | -------------------------------------------------------------------------------- /src/store/plugin/actions.js: -------------------------------------------------------------------------------- 1 | import PluginService from './pluginService' 2 | import { 3 | getPersistedPluginSettings, 4 | getPersistedFlags, 5 | getDefaultSetting, 6 | getPluginSettingsConfig, 7 | getSettingsIds 8 | } from '../../lib/utils' 9 | import { generateFlags } from '../../lib/flags' 10 | 11 | export const onConnectionUpdate = (pluginName, status) => { 12 | return { type: 'PLUGIN:STATUS_UPDATE', payload: { pluginName, status } } 13 | } 14 | 15 | const buildPluginSettings = (plugin, restoreDefaults = false) => { 16 | const pluginDefaults = {} 17 | 18 | if (!restoreDefaults) { 19 | const settingsIds = getSettingsIds(plugin) 20 | // Handle rehydration: if config.json has settings already, use them. 21 | const persistedSettings = getPersistedPluginSettings(plugin.name) 22 | if (settingsIds.length && persistedSettings) { 23 | if (Object.keys(persistedSettings).length) { 24 | settingsIds.forEach(id => { 25 | pluginDefaults[id] = 26 | persistedSettings[id] || getDefaultSetting(plugin, id) 27 | }) 28 | return pluginDefaults 29 | } 30 | } 31 | } 32 | 33 | const pluginSettings = getPluginSettingsConfig(plugin) 34 | pluginSettings.forEach(setting => { 35 | if ('default' in setting) { 36 | pluginDefaults[setting.id] = setting.default 37 | } 38 | }) 39 | 40 | return pluginDefaults 41 | } 42 | 43 | export const getGeneratedFlags = (plugin, config) => { 44 | const settings = getPluginSettingsConfig(plugin) 45 | return generateFlags(config, settings) 46 | } 47 | 48 | export const initPlugin = plugin => { 49 | return dispatch => { 50 | const config = buildPluginSettings(plugin) 51 | const pluginData = plugin.plugin.config 52 | const flags = 53 | getPersistedFlags(plugin.name) || getGeneratedFlags(plugin, config) 54 | const release = plugin.plugin.getSelectedRelease() 55 | 56 | dispatch({ 57 | type: 'PLUGIN:INIT', 58 | payload: { 59 | pluginName: pluginData.name, 60 | type: plugin.type, 61 | pluginData, 62 | config, 63 | flags, 64 | release 65 | } 66 | }) 67 | 68 | PluginService.createNewStateListener(plugin, dispatch) 69 | 70 | if (plugin.isRunning) { 71 | console.log('Resuming ', plugin.name) 72 | PluginService.resume(plugin, dispatch) 73 | } 74 | } 75 | } 76 | 77 | export const dismissFlagWarning = () => { 78 | return { type: 'PLUGIN:DISMISS_FLAG_WARNING' } 79 | } 80 | 81 | export const newBlock = (pluginName, blockNumber, timestamp) => { 82 | return { 83 | type: 'PLUGIN:UPDATE_NEW_BLOCK', 84 | payload: { pluginName, blockNumber, timestamp } 85 | } 86 | } 87 | 88 | export const updateSyncing = ( 89 | pluginName, 90 | { startingBlock, currentBlock, highestBlock, knownStates, pulledStates } 91 | ) => { 92 | return { 93 | type: 'PLUGIN:UPDATE_SYNCING', 94 | payload: { 95 | pluginName, 96 | startingBlock, 97 | currentBlock, 98 | highestBlock, 99 | knownStates, 100 | pulledStates 101 | } 102 | } 103 | } 104 | 105 | export const clearSyncing = pluginName => { 106 | return { 107 | type: 'PLUGIN:CLEAR_SYNCING', 108 | payload: { 109 | pluginName 110 | } 111 | } 112 | } 113 | 114 | export const updatePeerCount = (pluginName, peerCount) => { 115 | return (dispatch, getState) => { 116 | if (peerCount !== getState().plugin[pluginName].active.peerCount) { 117 | dispatch({ 118 | type: 'PLUGIN:UPDATE_PEER_COUNT', 119 | payload: { pluginName, peerCount } 120 | }) 121 | } 122 | } 123 | } 124 | 125 | export const updatePeerCountError = (pluginName, message) => { 126 | return { 127 | type: 'PLUGIN:UPDATE_PEER_COUNT:ERROR', 128 | error: true, 129 | payload: { pluginName, message } 130 | } 131 | } 132 | 133 | export const addPluginError = (pluginName, error) => { 134 | return (dispatch, getState) => { 135 | const state = getState() 136 | if (state.plugin[pluginName].errors.find(e => e.key === error.key)) { 137 | return 138 | } 139 | dispatch({ type: 'PLUGIN:ERROR:ADD', error, payload: { pluginName } }) 140 | } 141 | } 142 | 143 | export const getPluginErrors = plugin => { 144 | return dispatch => { 145 | plugin.getErrors().forEach(error => { 146 | dispatch(addPluginError(plugin.name, error)) 147 | }) 148 | } 149 | } 150 | 151 | export const clearError = (plugin, key) => { 152 | plugin.plugin.dismissError(key) 153 | return { 154 | type: 'PLUGIN:ERROR:CLEAR', 155 | payload: { pluginName: plugin.name, key } 156 | } 157 | } 158 | 159 | export const clearPluginErrors = pluginName => { 160 | return { type: 'PLUGIN:ERROR:CLEAR_ALL', payload: { pluginName } } 161 | } 162 | 163 | export const selectPlugin = (pluginName, tab) => { 164 | return { type: 'PLUGIN:SELECT', payload: { pluginName, tab } } 165 | } 166 | 167 | export const selectTab = tab => { 168 | return { type: 'PLUGIN:SELECT_TAB', payload: { tab } } 169 | } 170 | 171 | export const setRelease = (plugin, release) => { 172 | plugin.plugin.setSelectedRelease(release) 173 | return { 174 | type: 'PLUGIN:SET_RELEASE', 175 | payload: { pluginName: plugin.name, release } 176 | } 177 | } 178 | 179 | export const setFlags = (plugin, config) => { 180 | const pluginName = plugin.name 181 | const flags = getGeneratedFlags(plugin, config) 182 | return { type: 'PLUGIN:SET_FLAGS', payload: { pluginName, flags } } 183 | } 184 | 185 | export const setCustomFlags = (pluginName, flags) => { 186 | return { type: 'PLUGIN:SET_FLAGS', payload: { pluginName, flags } } 187 | } 188 | 189 | export const setConfig = (plugin, config) => { 190 | return dispatch => { 191 | const pluginName = plugin.name 192 | dispatch({ 193 | type: 'PLUGIN:SET_CONFIG', 194 | payload: { pluginName, config } 195 | }) 196 | dispatch(setFlags(plugin, config)) 197 | } 198 | } 199 | 200 | export const restoreDefaultSettings = plugin => { 201 | return dispatch => { 202 | const config = buildPluginSettings(plugin, true) 203 | dispatch(setConfig(plugin, config)) 204 | } 205 | } 206 | 207 | export const startPlugin = (plugin, release) => { 208 | return (dispatch, getState) => { 209 | try { 210 | const { config, flags } = getState().plugin[plugin.name] 211 | PluginService.start(plugin, release, flags, config, dispatch) 212 | return dispatch({ 213 | type: 'PLUGIN:START', 214 | payload: { pluginName: plugin.name, version: release.version, config } 215 | }) 216 | } catch (error) { 217 | return dispatch({ type: 'PLUGIN:START:ERROR', error: error.toString() }) 218 | } 219 | } 220 | } 221 | 222 | export const stopPlugin = plugin => { 223 | return dispatch => { 224 | try { 225 | PluginService.stop(plugin) 226 | dispatch({ type: 'PLUGIN:STOP', payload: { pluginName: plugin.name } }) 227 | } catch (e) { 228 | dispatch({ type: 'PLUGIN:STOP:ERROR', error: e.toString() }) 229 | } 230 | } 231 | } 232 | 233 | export const togglePlugin = (plugin, release) => { 234 | return async dispatch => { 235 | try { 236 | if (plugin.isRunning) { 237 | return dispatch(stopPlugin(plugin)) 238 | } 239 | return dispatch(startPlugin(plugin, release)) 240 | } catch (e) { 241 | return { type: 'PLUGIN:TOGGLE:ERROR', error: e.toString() } 242 | } 243 | } 244 | } 245 | 246 | export const setAppBadges = (plugin, appBadges = {}) => { 247 | return { 248 | type: 'PLUGIN:SET_APP_BADGES', 249 | payload: { pluginName: plugin.name, appBadges } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/store/plugin/clientService.js: -------------------------------------------------------------------------------- 1 | import { BigNumber } from 'bignumber.js' 2 | import { 3 | newBlock, 4 | updateSyncing, 5 | updatePeerCount, 6 | updatePeerCountError, 7 | clearSyncing 8 | } from './actions' 9 | 10 | // Utils 11 | const isHex = str => typeof str === 'string' && str.startsWith('0x') 12 | const hexToNumberString = str => new BigNumber(str).toString(10) 13 | const toNumberString = str => (isHex(str) ? hexToNumberString(str) : str) 14 | const hexToNumber = str => Number(hexToNumberString(str)) 15 | 16 | class ClientService { 17 | watchForPeers(plugin, dispatch) { 18 | this.peerCountInterval = setInterval( 19 | () => this.updatePeerCount(plugin, dispatch), 20 | 3000 21 | ) 22 | } 23 | 24 | async updatePeerCount(plugin, dispatch) { 25 | const hexPeerCount = await plugin.rpc('net_peerCount') 26 | if (hexPeerCount.message) { 27 | dispatch(updatePeerCountError(plugin.name, hexPeerCount.message)) 28 | } else { 29 | const peerCount = hexToNumber(hexPeerCount) 30 | dispatch(updatePeerCount(plugin.name, peerCount)) 31 | } 32 | } 33 | 34 | clearPeerCountInterval() { 35 | clearInterval(this.peerCountInterval) 36 | } 37 | 38 | onNewHeadsSubscriptionResult(plugin, result, dispatch) { 39 | const { result: subscriptionResult } = result 40 | if (!subscriptionResult) return 41 | 42 | const { 43 | number: hexBlockNumber, 44 | timestamp: hexTimestamp 45 | } = subscriptionResult 46 | const blockNumber = Number(toNumberString(hexBlockNumber)) 47 | const timestamp = Number(toNumberString(hexTimestamp)) 48 | dispatch(newBlock(plugin.name, blockNumber, timestamp)) 49 | } 50 | 51 | onSyncingResult(plugin, result, dispatch) { 52 | if (result === false) { 53 | // Stopped syncing, begin newHeads subscription 54 | this.startNewHeadsSubscription(plugin, dispatch) 55 | // Clear syncing interval 56 | this.clearSyncingInterval() 57 | return 58 | } 59 | 60 | // TODO: client is otherwise not correctly removing interval 61 | if (result.startingBlock === undefined) { 62 | this.clearSyncingInterval() 63 | } 64 | 65 | // Waiting for syncing data 66 | if (result === true) return 67 | 68 | const { 69 | startingBlock, 70 | currentBlock, 71 | highestBlock, 72 | knownStates, 73 | pulledStates 74 | } = result 75 | 76 | dispatch( 77 | updateSyncing(plugin.name, { 78 | startingBlock: hexToNumber(startingBlock), 79 | currentBlock: hexToNumber(currentBlock), 80 | highestBlock: hexToNumber(highestBlock), 81 | knownStates: hexToNumber(knownStates), 82 | pulledStates: hexToNumber(pulledStates) 83 | }) 84 | ) 85 | } 86 | 87 | unsubscribeNewHeadsSubscription(plugin) { 88 | if (!this.newHeadsSubscriptionId) return 89 | plugin.rpc('eth_unsubscribe', [this.newHeadsSubscriptionId]) 90 | this.newHeadsSubscriptionId = null 91 | } 92 | 93 | clearSyncingInterval() { 94 | clearInterval(this.syncingInterval) 95 | } 96 | 97 | startBlockSubscriptions(plugin, dispatch) { 98 | const startSubscriptions = async () => { 99 | const result = await plugin.rpc('eth_syncing') 100 | if (result) { 101 | // Subscribe to syncing 102 | this.startSyncingInterval(plugin, dispatch) 103 | } else { 104 | // Not syncing, start newHeads subscription 105 | this.startNewHeadsSubscription(plugin, dispatch) 106 | } 107 | } 108 | 109 | const setLastBlock = () => { 110 | plugin.rpc('eth_getBlockByNumber', ['latest', false]).then(block => { 111 | const { number: hexBlockNumber, timestamp: hexTimestamp } = block 112 | const blockNumber = hexToNumber(hexBlockNumber) 113 | const timestamp = hexToNumber(hexTimestamp) 114 | dispatch(newBlock(plugin.name, blockNumber, timestamp)) 115 | }) 116 | } 117 | 118 | const start = async () => { 119 | // Start if we have peers 120 | const hexPeerCount = await plugin.rpc('net_peerCount') 121 | if (!hexPeerCount.message && hexToNumber(hexPeerCount) > 0) { 122 | // Wait 5s before starting to give time for syncing status to update 123 | setTimeout(() => { 124 | setLastBlock() 125 | startSubscriptions() 126 | }, 5000) 127 | } else { 128 | // Otherwise, try again in 3s 129 | setTimeout(() => { 130 | start() 131 | }, 3000) 132 | } 133 | } 134 | 135 | start() 136 | } 137 | 138 | async startNewHeadsSubscription(plugin, dispatch) { 139 | // Clear any stale syncing data 140 | dispatch(clearSyncing(plugin.name)) 141 | 142 | // Subscribe 143 | const subscriptionId = await plugin.rpc('eth_subscribe', ['newHeads']) 144 | this.newHeadsSubscriptionId = subscriptionId 145 | plugin.on('notification', result => { 146 | const { subscription } = result 147 | if (subscription === this.newHeadsSubscriptionId) { 148 | this.onNewHeadsSubscriptionResult(plugin, result, dispatch) 149 | } 150 | }) 151 | } 152 | 153 | async startSyncingInterval(plugin, dispatch) { 154 | // Parity doesn't support eth_subscribe('syncing') yet and 155 | // geth wasn't returning results reliably, so for now we will poll. 156 | this.syncingInterval = setInterval(async () => { 157 | const result = await plugin.rpc('eth_syncing') 158 | this.onSyncingResult(plugin, result, dispatch) 159 | }, 3000) 160 | } 161 | } 162 | 163 | export default new ClientService() 164 | -------------------------------------------------------------------------------- /src/store/plugin/pluginService.js: -------------------------------------------------------------------------------- 1 | import ClientService from './clientService' 2 | import { 3 | addPluginError, 4 | onConnectionUpdate, 5 | setAppBadges, 6 | clearPluginErrors 7 | } from './actions' 8 | 9 | class PluginService { 10 | async start(plugin, release, flags, config) { 11 | if (!release.location) { 12 | release = null // eslint-disable-line 13 | } 14 | await plugin.start(flags, release, config) 15 | } 16 | 17 | resume(plugin, dispatch) { 18 | dispatch(onConnectionUpdate(plugin.name, plugin.state)) 19 | 20 | // `resume` is called for 'STARTED', 'STARTING', and 'CONNECTED' states. 21 | // If plugin starts as 'CONNECTED', manually trigger `createListeners` and `onConnect`. 22 | if (plugin.state === 'CONNECTED') { 23 | this.createListeners(plugin, dispatch) 24 | this.onConnect(plugin, dispatch) 25 | } 26 | } 27 | 28 | stop(plugin) { 29 | plugin.stop() 30 | } 31 | 32 | onConnect(plugin, dispatch) { 33 | if (plugin.type === 'client') { 34 | ClientService.watchForPeers(plugin, dispatch) 35 | ClientService.startBlockSubscriptions(plugin, dispatch) 36 | } 37 | } 38 | 39 | // Called in `initPlugin` 40 | createNewStateListener(plugin, dispatch) { 41 | this.newStateListener = newState => { 42 | dispatch(onConnectionUpdate(plugin.name, newState.toUpperCase())) 43 | switch (newState) { 44 | case 'starting': 45 | this.createListeners(plugin, dispatch) 46 | break 47 | case 'connected': 48 | this.onConnect(plugin, dispatch) 49 | break 50 | case 'stopping': 51 | this.removeListeners(plugin) 52 | break 53 | default: 54 | break 55 | } 56 | } 57 | plugin.on('newState', this.newStateListener) 58 | } 59 | 60 | createListeners(plugin, dispatch) { 61 | this.pluginErrorListener = error => { 62 | dispatch(addPluginError(plugin.name, error)) 63 | } 64 | this.clearPluginErrorsListener = () => { 65 | dispatch(clearPluginErrors(plugin.name)) 66 | } 67 | this.setAppBadgeListener = ({ appId, count }) => { 68 | dispatch(setAppBadges(plugin, { [appId]: count })) 69 | } 70 | plugin.on('pluginError', this.pluginErrorListener) 71 | plugin.on('clearPluginErrors', this.clearPluginErrorsListener) 72 | plugin.on('setAppBadge', this.setAppBadgeListener) 73 | } 74 | 75 | removeListeners(plugin) { 76 | plugin.removeListener('pluginError', this.pluginErrorListener) 77 | plugin.removeListener('clearPluginErrors', this.clearPluginErrorsListener) 78 | plugin.removeListener('setAppBadge', this.setAppBadgeListener) 79 | if (plugin.type === 'client') { 80 | ClientService.clearPeerCountInterval() 81 | ClientService.clearSyncingInterval(plugin) 82 | ClientService.unsubscribeNewHeadsSubscription(plugin) 83 | } 84 | } 85 | } 86 | 87 | export default new PluginService() 88 | -------------------------------------------------------------------------------- /src/store/plugin/reducer.js: -------------------------------------------------------------------------------- 1 | export const initialState = { 2 | selected: 'geth', 3 | selectedTab: 0, 4 | showCustomFlagWarning: true 5 | // Plugins dynamically populate within this object, e.g. 6 | // geth: { config: {}, release: {}, ... }, 7 | // parity: { config: {}, release: {}, ... }, 8 | } 9 | 10 | export const initialPluginState = { 11 | active: { 12 | blockNumber: null, 13 | peerCount: 0, 14 | status: 'STOPPED', 15 | sync: { 16 | currentBlock: 0, 17 | highestBlock: 0, 18 | knownStates: 0, 19 | pulledStates: 0, 20 | startingBlock: 0 21 | }, 22 | timestamp: null, 23 | version: null 24 | }, 25 | binaryName: '', 26 | config: {}, 27 | flags: [], 28 | displayName: '', 29 | errors: [], 30 | appBadges: {}, 31 | name: '', 32 | prefix: '', 33 | release: { 34 | name: null, 35 | fileName: null, 36 | version: null, 37 | tag: null, 38 | size: null, 39 | location: null, 40 | checksums: null, 41 | signature: null, 42 | remote: false 43 | }, 44 | repository: '', 45 | type: '' 46 | } 47 | 48 | const plugin = (state = initialState, action) => { 49 | switch (action.type) { 50 | case 'PLUGIN:INIT': { 51 | const { 52 | pluginName, 53 | pluginData, 54 | config, 55 | type, 56 | flags, 57 | release 58 | } = action.payload 59 | const newState = { 60 | ...state, 61 | [pluginName]: { 62 | ...initialPluginState, 63 | ...pluginData, 64 | config, 65 | type, 66 | flags 67 | } 68 | } 69 | if (release) { 70 | newState[pluginName].release = release 71 | } 72 | return newState 73 | } 74 | case 'PLUGIN:SELECT': { 75 | const { pluginName, tab } = action.payload 76 | const newState = { ...state, selected: pluginName } 77 | if (tab) { 78 | newState.selectedTab = tab 79 | } 80 | return newState 81 | } 82 | case 'PLUGIN:SELECT_TAB': { 83 | const { tab } = action.payload 84 | return { ...state, selectedTab: tab } 85 | } 86 | case 'PLUGIN:SET_RELEASE': { 87 | const { pluginName, release } = action.payload 88 | return { 89 | ...state, 90 | [pluginName]: { ...initialPluginState, ...state[pluginName], release } 91 | } 92 | } 93 | case 'PLUGIN:SET_CONFIG': { 94 | const { pluginName, config } = action.payload 95 | return { 96 | ...state, 97 | [pluginName]: { 98 | ...initialPluginState, 99 | ...state[pluginName], 100 | config 101 | } 102 | } 103 | } 104 | case 'PLUGIN:SET_FLAGS': { 105 | const { pluginName, flags } = action.payload 106 | return { 107 | ...state, 108 | [pluginName]: { 109 | ...initialPluginState, 110 | ...state[pluginName], 111 | flags 112 | } 113 | } 114 | } 115 | case 'PLUGIN:DISMISS_FLAG_WARNING': { 116 | return { ...state, showCustomFlagWarning: false } 117 | } 118 | case 'PLUGIN:START': { 119 | const { pluginName, version } = action.payload 120 | const activeState = state[pluginName] 121 | ? state[pluginName].active 122 | : initialPluginState.active 123 | 124 | return { 125 | ...state, 126 | [pluginName]: { 127 | ...initialPluginState, 128 | ...state[pluginName], 129 | active: { ...activeState, version }, 130 | errors: [] 131 | } 132 | } 133 | } 134 | case 'PLUGIN:STATUS_UPDATE': { 135 | const { pluginName, status } = action.payload 136 | const activeState = state[pluginName] 137 | ? state[pluginName].active 138 | : initialPluginState.active 139 | 140 | return { 141 | ...state, 142 | [pluginName]: { 143 | ...initialPluginState, 144 | ...state[pluginName], 145 | active: { ...activeState, status } 146 | } 147 | } 148 | } 149 | case 'PLUGIN:STOP': { 150 | const { pluginName } = action.payload 151 | return { 152 | ...state, 153 | [pluginName]: { 154 | ...initialPluginState, 155 | ...state[pluginName], 156 | active: { ...initialPluginState.active } 157 | } 158 | } 159 | } 160 | case 'PLUGIN:ERROR:ADD': { 161 | const { payload, error } = action 162 | const { pluginName } = payload 163 | return { 164 | ...state, 165 | [pluginName]: { 166 | ...initialPluginState, 167 | ...state[pluginName], 168 | errors: [...state[pluginName].errors, error] 169 | } 170 | } 171 | } 172 | case 'PLUGIN:ERROR:CLEAR': { 173 | const { pluginName, key } = action.payload 174 | 175 | return { 176 | ...state, 177 | [pluginName]: { 178 | ...initialPluginState, 179 | ...state[pluginName], 180 | errors: state[pluginName].errors.filter(error => error.key !== key) 181 | } 182 | } 183 | } 184 | case 'PLUGIN:ERROR:CLEAR_ALL': { 185 | const { pluginName } = action.payload 186 | return { 187 | ...state, 188 | [pluginName]: { 189 | ...initialPluginState, 190 | ...state[pluginName], 191 | errors: [] 192 | } 193 | } 194 | } 195 | case 'PLUGIN:SET_APP_BADGES': { 196 | const { pluginName, appBadges } = action.payload 197 | return { 198 | ...state, 199 | [pluginName]: { 200 | ...initialPluginState, 201 | ...state[pluginName], 202 | appBadges: { 203 | ...state[pluginName].appBadges, 204 | ...appBadges 205 | } 206 | } 207 | } 208 | } 209 | case 'PLUGIN:UPDATE_NEW_BLOCK': { 210 | const { pluginName, blockNumber, timestamp } = action.payload 211 | const activeState = state[pluginName] 212 | ? state[pluginName].active 213 | : initialPluginState.active 214 | 215 | return { 216 | ...state, 217 | [pluginName]: { 218 | ...initialPluginState, 219 | ...state[pluginName], 220 | active: { ...activeState, blockNumber, timestamp } 221 | } 222 | } 223 | } 224 | case 'PLUGIN:UPDATE_SYNCING': { 225 | const { 226 | pluginName, 227 | startingBlock, 228 | currentBlock, 229 | highestBlock, 230 | knownStates, 231 | pulledStates 232 | } = action.payload 233 | const activeState = state[pluginName] 234 | ? state[pluginName].active 235 | : initialPluginState.active 236 | 237 | return { 238 | ...state, 239 | [pluginName]: { 240 | ...initialPluginState, 241 | ...state[pluginName], 242 | active: { 243 | ...activeState, 244 | sync: { 245 | ...state.sync, 246 | startingBlock, 247 | currentBlock, 248 | highestBlock, 249 | knownStates, 250 | pulledStates 251 | } 252 | } 253 | } 254 | } 255 | } 256 | case 'PLUGIN:CLEAR_SYNCING': { 257 | const { pluginName } = action.payload 258 | const activeState = state[pluginName] 259 | ? state[pluginName].active 260 | : initialPluginState.active 261 | 262 | return { 263 | ...state, 264 | [pluginName]: { 265 | ...initialPluginState, 266 | ...state[pluginName], 267 | active: { 268 | ...activeState, 269 | sync: { 270 | ...initialPluginState.active.sync 271 | } 272 | } 273 | } 274 | } 275 | } 276 | case 'PLUGIN:UPDATE_PEER_COUNT': { 277 | const { pluginName, peerCount } = action.payload 278 | const activeState = state[pluginName] 279 | ? state[pluginName].active 280 | : initialPluginState.active 281 | 282 | return { 283 | ...state, 284 | [pluginName]: { 285 | ...initialPluginState, 286 | ...state[pluginName], 287 | active: { ...activeState, peerCount } 288 | } 289 | } 290 | } 291 | default: 292 | return state 293 | } 294 | } 295 | 296 | export default plugin 297 | -------------------------------------------------------------------------------- /src/store/plugin/reducer.test.js: -------------------------------------------------------------------------------- 1 | import reducer, { initialState, initialPluginState } from './reducer' 2 | 3 | describe('the plugin reducer', () => { 4 | it('should handle PLUGIN:INIT', () => { 5 | const action = { 6 | type: 'PLUGIN:INIT', 7 | payload: { 8 | pluginName: 'parity', 9 | pluginData: { 10 | name: 'parity', 11 | displayName: 'Parity', 12 | config: { default: { sync: 'warp' } } 13 | }, 14 | config: { sync: 'warp' }, 15 | flags: [], 16 | type: 'client' 17 | } 18 | } 19 | const expectedState = { 20 | ...initialState, 21 | parity: { 22 | ...initialPluginState, 23 | name: 'parity', 24 | displayName: 'Parity', 25 | config: { sync: 'warp' }, 26 | flags: [], 27 | type: 'client' 28 | } 29 | } 30 | 31 | expect(reducer(initialState, action)).toEqual(expectedState) 32 | }) 33 | 34 | it('should handle PLUGIN:SELECT', () => { 35 | const action = { 36 | type: 'PLUGIN:SELECT', 37 | payload: { pluginName: 'parity', tab: 0 } 38 | } 39 | const expectedState = { ...initialState, selected: 'parity' } 40 | 41 | expect(reducer(initialState, action)).toEqual(expectedState) 42 | }) 43 | 44 | it('should handle PLUGIN:DISMISS_FLAG_WARNING', () => { 45 | const action = { type: 'PLUGIN:DISMISS_FLAG_WARNING' } 46 | const expectedState = { ...initialState, showCustomFlagWarning: false } 47 | 48 | expect(reducer(initialState, action)).toEqual(expectedState) 49 | }) 50 | 51 | it('should handle PLUGIN:SELECT_TAB', () => { 52 | const action = { 53 | type: 'PLUGIN:SELECT_TAB', 54 | payload: { tab: 2 } 55 | } 56 | const expectedState = { ...initialState, selectedTab: 2 } 57 | 58 | expect(reducer(initialState, action)).toEqual(expectedState) 59 | }) 60 | 61 | it('should handle PLUGIN:SET_CONFIG', () => { 62 | const config = { 63 | name: 'default', 64 | dataDir: '/example', 65 | host: 'example', 66 | port: '1234', 67 | network: 'rinkeby', 68 | syncMode: 'light', 69 | ipc: 'websockets' 70 | } 71 | const action = { 72 | type: 'PLUGIN:SET_CONFIG', 73 | payload: { pluginName: 'geth', config } 74 | } 75 | const expectedState = { 76 | ...initialState, 77 | geth: { ...initialPluginState, config } 78 | } 79 | 80 | expect(reducer(initialState, action)).toEqual(expectedState) 81 | }) 82 | 83 | it('should handle PLUGIN:START', () => { 84 | const action = { 85 | type: 'PLUGIN:START', 86 | payload: { 87 | pluginName: 'geth', 88 | version: '1.X.X' 89 | } 90 | } 91 | const expectedState = { 92 | ...initialState, 93 | geth: { 94 | ...initialPluginState, 95 | active: { ...initialPluginState.active, version: '1.X.X' } 96 | } 97 | } 98 | 99 | expect(reducer(initialState, action)).toEqual(expectedState) 100 | }) 101 | 102 | it('should handle PLUGIN:STATUS_UPDATE', () => { 103 | const action = { 104 | type: 'PLUGIN:STATUS_UPDATE', 105 | payload: { 106 | pluginName: 'geth', 107 | status: 'STARTED' 108 | } 109 | } 110 | const expectedState = { 111 | ...initialState, 112 | geth: { 113 | ...initialPluginState, 114 | active: { ...initialPluginState.active, status: 'STARTED' } 115 | } 116 | } 117 | 118 | expect(reducer(initialState, action)).toEqual(expectedState) 119 | }) 120 | 121 | it('should handle PLUGIN:STOP', () => { 122 | const action = { 123 | type: 'PLUGIN:STOP', 124 | payload: { pluginName: 'geth' } 125 | } 126 | const expectedState = { 127 | ...initialState, 128 | geth: { ...initialPluginState } 129 | } 130 | 131 | expect(reducer(initialState, action)).toEqual(expectedState) 132 | }) 133 | 134 | it('should handle PLUGIN:ERROR:ADD', () => { 135 | const action = { 136 | type: 'PLUGIN:ERROR:ADD', 137 | error: 'Boom', 138 | payload: { pluginName: 'geth' } 139 | } 140 | const expectedState = { 141 | ...initialState, 142 | geth: { 143 | ...initialPluginState, 144 | errors: ['Boom'] 145 | } 146 | } 147 | 148 | expect( 149 | reducer({ ...initialState, geth: initialPluginState }, action) 150 | ).toEqual(expectedState) 151 | }) 152 | 153 | it('should handle PLUGIN:ERROR:CLEAR', () => { 154 | const action = { 155 | type: 'PLUGIN:ERROR:CLEAR', 156 | payload: { pluginName: 'geth', key: 'abc' } 157 | } 158 | const expectedState = { 159 | ...initialState, 160 | geth: { 161 | ...initialPluginState, 162 | errors: [{ key: 'abcd', message: 'boom2' }] 163 | } 164 | } 165 | 166 | expect( 167 | reducer( 168 | { 169 | ...initialState, 170 | geth: { 171 | ...initialPluginState, 172 | errors: [ 173 | { key: 'abc', message: 'boom' }, 174 | { key: 'abcd', message: 'boom2' } 175 | ] 176 | } 177 | }, 178 | action 179 | ) 180 | ).toEqual(expectedState) 181 | }) 182 | 183 | it('should handle PLUGIN:ERROR:CLEAR_ALL', () => { 184 | const action = { 185 | type: 'PLUGIN:ERROR:CLEAR_ALL', 186 | payload: { pluginName: 'geth' } 187 | } 188 | const expectedState = { 189 | ...initialState, 190 | geth: { ...initialPluginState, errors: [] } 191 | } 192 | 193 | expect( 194 | reducer( 195 | { 196 | ...initialState, 197 | geth: { 198 | ...initialPluginState, 199 | errors: [ 200 | { key: 'abc', message: 'boom' }, 201 | { key: 'abcd', message: 'boom2' } 202 | ] 203 | } 204 | }, 205 | action 206 | ) 207 | ).toEqual(expectedState) 208 | }) 209 | 210 | it('should handle PLUGIN:UPDATE_PEER_COUNT', () => { 211 | const action = { 212 | type: 'PLUGIN:UPDATE_PEER_COUNT', 213 | payload: { pluginName: 'geth', peerCount: '3' } 214 | } 215 | const expectedState = { 216 | ...initialState, 217 | geth: { 218 | ...initialPluginState, 219 | active: { ...initialPluginState.active, peerCount: '3' } 220 | } 221 | } 222 | 223 | expect(reducer(initialState, action)).toEqual(expectedState) 224 | }) 225 | 226 | it('should handle PLUGIN:SET_RELEASE', () => { 227 | const release = { 228 | name: 'example', 229 | fileName: 'example', 230 | version: '1.X.X', 231 | tag: 'alpha', 232 | size: 'example', 233 | location: 'example', 234 | checksums: 'example', 235 | signature: 'example' 236 | } 237 | const action = { 238 | type: 'PLUGIN:SET_RELEASE', 239 | payload: { pluginName: 'geth', release } 240 | } 241 | const expectedState = { 242 | ...initialState, 243 | geth: { ...initialPluginState, release } 244 | } 245 | 246 | expect(reducer(initialState, action)).toEqual(expectedState) 247 | }) 248 | 249 | it('should handle PLUGIN:UPDATE_NEW_BLOCK', () => { 250 | const blockNumber = '123123' 251 | const timestamp = '321321321' 252 | const action = { 253 | type: 'PLUGIN:UPDATE_NEW_BLOCK', 254 | payload: { pluginName: 'geth', blockNumber, timestamp } 255 | } 256 | const expectedState = { 257 | ...initialState, 258 | geth: { 259 | ...initialPluginState, 260 | active: { ...initialPluginState.active, blockNumber, timestamp } 261 | } 262 | } 263 | 264 | expect(reducer(initialState, action)).toEqual(expectedState) 265 | }) 266 | 267 | it('should handle PLUGIN:UPDATE_SYNCING', () => { 268 | const sync = { 269 | currentBlock: 123, 270 | highestBlock: 124, 271 | knownStates: 125, 272 | pulledStates: 124, 273 | startingBlock: 0 274 | } 275 | const action = { 276 | type: 'PLUGIN:UPDATE_SYNCING', 277 | payload: { pluginName: 'geth', ...sync } 278 | } 279 | const expectedState = { 280 | ...initialState, 281 | geth: { 282 | ...initialPluginState, 283 | active: { ...initialPluginState.active, sync } 284 | } 285 | } 286 | 287 | expect(reducer(initialState, action)).toEqual(expectedState) 288 | }) 289 | 290 | it('should handle PLUGIN:CLEAR_SYNCING', () => { 291 | const sync = { 292 | currentBlock: 123, 293 | highestBlock: 124, 294 | knownStates: 125, 295 | pulledStates: 124, 296 | startingBlock: 5 297 | } 298 | const action = { 299 | type: 'PLUGIN:CLEAR_SYNCING', 300 | payload: { pluginName: 'geth' } 301 | } 302 | const expectedState = { 303 | ...initialState, 304 | geth: { 305 | ...initialPluginState, 306 | active: { 307 | ...initialPluginState.active, 308 | sync: initialPluginState.active.sync 309 | } 310 | } 311 | } 312 | 313 | expect( 314 | reducer( 315 | { 316 | ...initialState, 317 | geth: { 318 | ...initialPluginState, 319 | active: { ...initialPluginState.active, sync } 320 | } 321 | }, 322 | action 323 | ) 324 | ).toEqual(expectedState) 325 | }) 326 | }) 327 | -------------------------------------------------------------------------------- /src/store/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import plugin from './plugin/reducer' 3 | 4 | const rootReducer = combineReducers({ 5 | plugin 6 | }) 7 | 8 | export default rootReducer 9 | -------------------------------------------------------------------------------- /src/test/settings.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | getDefaultSetting, 3 | getPluginSettingsConfig, 4 | getSettingsIds 5 | } from '../lib/utils' 6 | import { generateFlags } from '../lib/flags' 7 | 8 | describe('getPluginSettingsConfig', () => { 9 | it('returns an empty array if no plugin', () => { 10 | const plugin = undefined 11 | expect(getPluginSettingsConfig(plugin)).toEqual([]) 12 | }) 13 | 14 | it('returns an empty array if no settings config', () => { 15 | const plugin = { plugin: { config: {} } } 16 | expect(getPluginSettingsConfig(plugin)).toEqual([]) 17 | }) 18 | 19 | it('returns an empty array if settings are not an array', () => { 20 | const plugin = { plugin: { config: { settings: { one: '1', two: '2' } } } } 21 | expect(getPluginSettingsConfig(plugin)).toEqual([]) 22 | }) 23 | 24 | it('returns the array of settings', () => { 25 | const settings = [{ id: 'one' }, { id: 'two' }] 26 | const plugin = { plugin: { config: { settings } } } 27 | expect(getPluginSettingsConfig(plugin)).toEqual(settings) 28 | }) 29 | }) 30 | 31 | describe('getDefaultSetting', () => { 32 | it('returns an empty string if no plugin', () => { 33 | const plugin = undefined 34 | expect(getDefaultSetting(plugin, 'network')).toEqual('') 35 | }) 36 | 37 | it('returns an empty string if no id', () => { 38 | const settings = [{ id: 'one', default: 'a' }, { id: 'two', default: 'b' }] 39 | const plugin = { plugin: { config: { settings } } } 40 | expect(getDefaultSetting(plugin, undefined)).toEqual('') 41 | }) 42 | 43 | it('returns the default value', () => { 44 | const settings = [{ id: 'one', default: 'a' }, { id: 'two', default: 'b' }] 45 | const plugin = { plugin: { config: { settings } } } 46 | expect(getDefaultSetting(plugin, 'two')).toEqual('b') 47 | }) 48 | }) 49 | 50 | describe('getSettingsIds', () => { 51 | it('returns an empty array if no plugin', () => { 52 | const plugin = undefined 53 | expect(getSettingsIds(plugin)).toEqual([]) 54 | }) 55 | 56 | it('returns an array of ids', () => { 57 | const settings = [{ id: 'one', default: 'a' }, { id: 'two', default: 'b' }] 58 | const plugin = { plugin: { config: { settings } } } 59 | expect(getSettingsIds(plugin)).toEqual(['one', 'two']) 60 | }) 61 | }) 62 | 63 | describe('generateFlags', () => { 64 | it('should handle an empty settings', () => { 65 | const input = {} 66 | const settings = [] 67 | const flags = generateFlags(input, settings) 68 | 69 | expect(flags).toEqual([]) 70 | }) 71 | 72 | it('should parse basic field', () => { 73 | const input = { network: '' } 74 | const settings = [{ id: 'network', flag: '--rinkeby' }] 75 | const flags = generateFlags(input, settings) 76 | 77 | expect(flags).toEqual(['--rinkeby']) 78 | }) 79 | 80 | it('should parse some basic fields', () => { 81 | const input = { 82 | network: '', 83 | debug: '', 84 | nodiscovery: '' 85 | } 86 | const settings = [ 87 | { id: 'network', flag: '--rinkeby' }, 88 | { id: 'debug', flag: '--debug' }, 89 | { id: 'nodiscovery', flag: '--no-discovery' } 90 | ] 91 | const flags = generateFlags(input, settings) 92 | 93 | expect(flags).toContain('--rinkeby') 94 | expect(flags).toContain('--debug') 95 | expect(flags).toContain('--no-discovery') 96 | }) 97 | 98 | it('should parse text values', () => { 99 | const input = { 100 | cache: '1024', 101 | syncmode: 'light' 102 | } 103 | const settings = [ 104 | { id: 'cache', flag: '--cache %s' }, 105 | { id: 'syncmode', flag: '--syncmode %s' } 106 | ] 107 | const flags = generateFlags(input, settings) 108 | 109 | expect(flags).toEqual(['--cache', '1024', '--syncmode', 'light']) 110 | }) 111 | 112 | it('should parse simple options', () => { 113 | const input = { 114 | syncmode: 'light' 115 | } 116 | const settings = [ 117 | { 118 | id: 'syncmode', 119 | options: ['fast', 'full', 'light'], 120 | flag: '--syncmode %s' 121 | } 122 | ] 123 | 124 | const flags = generateFlags(input, settings) 125 | expect(flags).toEqual(['--syncmode', 'light']) 126 | }) 127 | 128 | it('should parse full options', () => { 129 | const input = { 130 | network: 'rinkeby' 131 | } 132 | const settings = [ 133 | { 134 | id: 'network', 135 | default: 'main', 136 | options: [ 137 | { value: 'ropsten', flag: '--testnet' }, 138 | { value: 'rinkeby', flag: '--rinkeby' } 139 | ] 140 | } 141 | ] 142 | 143 | const flags = generateFlags(input, settings) 144 | expect(flags).toEqual(['--rinkeby']) 145 | }) 146 | 147 | it('full options should allow empty flags', () => { 148 | const input = { 149 | network: 'mainnet' 150 | } 151 | const settings = [ 152 | { 153 | id: 'network', 154 | options: [ 155 | { value: 'ropsten', flag: '--testnet' }, 156 | { value: 'mainnet', flag: '' } 157 | ] 158 | } 159 | ] 160 | 161 | const flags = generateFlags(input, settings) 162 | expect(flags).toEqual([]) 163 | }) 164 | 165 | it('should parse value with full options', () => { 166 | const input = { 167 | syncmode: 'light' 168 | } 169 | const settings = [ 170 | { 171 | id: 'syncmode', 172 | options: [ 173 | { value: 'fast', flag: '--syncmode %s' }, 174 | { value: 'light', flag: '--syncmode %s --maxpeers=100' } 175 | ] 176 | } 177 | ] 178 | 179 | const flags = generateFlags(input, settings) 180 | expect(flags).toEqual(['--syncmode', 'light', '--maxpeers=100']) 181 | }) 182 | 183 | it('should not split values with spaces', () => { 184 | const input = { ipcPath: '/path/with spaces.ipc' } 185 | const settings = [ 186 | { 187 | id: 'ipcPath', 188 | flag: '--ipc %s' 189 | } 190 | ] 191 | 192 | const flags = generateFlags(input, settings) 193 | expect(flags).toEqual(['--ipc', '/path/with spaces.ipc']) 194 | }) 195 | 196 | it('should ignore empty setting if ignoreIfEmpty is true', () => { 197 | const input = { dataDir: '' } 198 | const settings = [ 199 | { 200 | id: 'dataDir', 201 | default: '', 202 | label: 'Data Directory', 203 | flag: '--datadir %s', 204 | type: 'directory', 205 | ignoreIfEmpty: true 206 | } 207 | ] 208 | 209 | const flags = generateFlags(input, settings) 210 | expect(flags).toEqual([]) 211 | }) 212 | 213 | it('should output flag if value is a space and ignoreIfEmpty is true', () => { 214 | const input = { dataDir: ' ' } 215 | const settings = [ 216 | { 217 | id: 'dataDir', 218 | default: '', 219 | label: 'Data Directory', 220 | flag: '--datadir %s', 221 | type: 'directory', 222 | ignoreIfEmpty: true 223 | } 224 | ] 225 | 226 | const flags = generateFlags(input, settings) 227 | expect(flags).toEqual(['--datadir', ' ']) 228 | }) 229 | 230 | it('should not ignore empty setting if ignoreIfEmpty is undefined', () => { 231 | const input = { dataDir: '' } 232 | const settings = [ 233 | { 234 | id: 'dataDir', 235 | default: '', 236 | label: 'Data Directory', 237 | flag: '--datadir %s', 238 | type: 'directory' 239 | } 240 | ] 241 | 242 | const flags = generateFlags(input, settings) 243 | expect(flags).toEqual(['--datadir']) 244 | }) 245 | }) 246 | 247 | describe('generateFlags error handling', () => { 248 | it('should throw if settings is not an array', () => { 249 | const input = {} 250 | const settings = { 251 | cache: { flag: '--cache %s' }, 252 | syncmode: { flag: '--syncmode %s' } 253 | } 254 | 255 | expect(() => generateFlags(input, settings)).toThrow( 256 | 'Settings must be an Array instance' 257 | ) 258 | }) 259 | 260 | it('should throw for basic field without flag', () => { 261 | const input = { network: 'main' } 262 | const settings = [{ id: 'network' }] 263 | 264 | expect(() => generateFlags(input, settings)).toThrow( 265 | 'Config entry "network" must have the "flag" key' 266 | ) 267 | }) 268 | 269 | it('should throw for simple options without flag', () => { 270 | const input = { sync: 'fast' } 271 | const settings = [ 272 | { 273 | id: 'sync', 274 | options: ['light', 'fast', 'full'] 275 | } 276 | ] 277 | 278 | expect(() => generateFlags(input, settings)).toThrow( 279 | 'Option "fast" must have the "flag" key' 280 | ) 281 | }) 282 | 283 | it('should throw for full options without flag', () => { 284 | const input = { network: 'main' } 285 | const settings = [ 286 | { 287 | id: 'network', 288 | options: [{ value: 'main', label: 'Main' }] 289 | } 290 | ] 291 | 292 | expect(() => generateFlags(input, settings)).toThrow( 293 | 'Option "main" must have the "flag" key' 294 | ) 295 | }) 296 | }) 297 | -------------------------------------------------------------------------------- /src/theme.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { createMuiTheme } from '@material-ui/core/styles' 3 | 4 | export const primary = '#4fb783' // green 5 | export const primary2 = '#78aac7' // blue 6 | export const primary3 = '#5d63b3' // purple 7 | 8 | const darkTheme = createMuiTheme({ 9 | typography: { 10 | useNextVariants: true 11 | }, 12 | palette: { 13 | type: 'dark', 14 | 15 | primary: { 16 | main: primary 17 | }, 18 | secondary: { 19 | main: primary2 20 | }, 21 | background: { 22 | default: '#202225' 23 | } 24 | // primary: { 25 | // // light: will be calculated from palette.primary.main, 26 | // main: primary, 27 | // // dark: will be calculated from palette.primary.main, 28 | // contrastText: '#ffffff' // will be calculated to contrast with palette.primary.main 29 | // }, 30 | // secondary: { 31 | // // light: '#0066ff', 32 | // main: '#ffffff' 33 | // // dark: will be calculated from palette.secondary.main, 34 | // // contrastText: '#ffcc00', 35 | // // }, 36 | // // error: will use the default color 37 | // } 38 | }, 39 | overrides: { 40 | MuiAppBar: { 41 | colorPrimary: { 42 | backgroundColor: '#202225' 43 | } 44 | }, 45 | MuiTab: { 46 | textColorPrimary: { 47 | '&$disabled': { 48 | color: 'rgba(0, 0, 0, 0.2)' 49 | } 50 | } 51 | }, 52 | MuiDrawer: { 53 | paper: { 54 | backgroundColor: '#202225' 55 | } 56 | } 57 | } 58 | }) 59 | 60 | const lightTheme = createMuiTheme({ 61 | typography: { 62 | useNextVariants: true 63 | }, 64 | palette: { 65 | background: { 66 | default: '#ffffff' 67 | }, 68 | primary: { 69 | // light: will be calculated from palette.primary.main, 70 | main: primary, 71 | // dark: will be calculated from palette.primary.main, 72 | contrastText: '#ffffff' // will be calculated to contrast with palette.primary.main 73 | }, 74 | secondary: { 75 | // light: '#0066ff', 76 | main: '#ffffff' 77 | // dark: will be calculated from palette.secondary.main, 78 | // contrastText: '#ffcc00', 79 | // }, 80 | // error: will use the default color 81 | } 82 | }, 83 | overrides: { 84 | MuiAppBar: { 85 | colorPrimary: { 86 | backgroundColor: '#ffffff' 87 | } 88 | }, 89 | MuiTab: { 90 | textColorPrimary: { 91 | '&$disabled': { 92 | color: 'rgba(0, 0, 0, 0.2)' 93 | } 94 | } 95 | }, 96 | MuiDrawer: { 97 | paper: { 98 | backgroundColor: '#f2f2f2' 99 | } 100 | } 101 | } 102 | }) 103 | 104 | export { lightTheme, darkTheme } 105 | -------------------------------------------------------------------------------- /tasks.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | use a static glob pattern or task 4 | 5 | const fs = require('fs') 6 | async function copyFile(source, target) { 7 | var rd = fs.createReadStream(source); 8 | var wr = fs.createWriteStream(target); 9 | try { 10 | return await new Promise(function(resolve, reject) { 11 | rd.on('error', reject); 12 | wr.on('error', reject); 13 | wr.on('finish', resolve); 14 | rd.pipe(wr); 15 | }); 16 | } catch (error) { 17 | rd.destroy(); 18 | wr.end(); 19 | throw error; 20 | } 21 | } 22 | [ 23 | 'Copy Files', 24 | async () => { 25 | const basePath = process.cwd() 26 | let p = path.join(basePath, 'preload', 'preload.js') 27 | let dest = path.join(basePath, 'build', 'preload.js') 28 | await copyFile(p, dest) 29 | 30 | let i18n = path.join(basePath, 'build', 'i18n') 31 | if (!fs.existsSync(i18n)) { 32 | fs.mkdirSync(i18n) 33 | } 34 | 35 | let eng1 = path.join(basePath, 'i18n', 'app.en.i18n.json') 36 | await copyFile(eng1, path.join(i18n, 'app.en.i18n.json')) 37 | 38 | let eng2 = path.join(basePath, 'i18n', 'mist.en.i18n.json') 39 | await copyFile(eng2, path.join(i18n, 'mist.en.i18n.json')) 40 | 41 | return true 42 | } 43 | ], 44 | */ 45 | let tasks = [ 46 | [ 47 | "Copy i18n", 48 | async () => true 49 | ] 50 | ] 51 | 52 | module.exports = { 53 | postAppBuild: tasks 54 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //process.env.NODE_ENV = "production" 2 | 3 | let BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin 4 | 5 | module.exports = (craConfig, configType) => { 6 | // configType has a value of 'DEVELOPMENT' or 'PRODUCTION' 7 | // You can change the configuration based on that. 8 | 9 | console.log('customize webpack config') 10 | 11 | craConfig.plugins.push( 12 | new BundleAnalyzerPlugin({ 13 | analyzerMode: "static", 14 | reportFilename: "report.html", 15 | }) 16 | ) 17 | 18 | 19 | // Return the altered config 20 | return craConfig 21 | } 22 | --------------------------------------------------------------------------------