├── .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 |
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 | 
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 |
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 |
118 | )
119 | case 'file':
120 | case 'file_multiple':
121 | case 'directory':
122 | case 'directory_multiple':
123 | return (
124 |
125 | this.handleChange(itemKey, event.target.value)}
131 | placeholder={
132 | item.ignoreIfEmpty ? '(Leave empty to use default)' : ''
133 | }
134 | disabled={disabled}
135 | InputProps={{
136 | endAdornment: (
137 |
138 | {
142 | if (
143 | this.inputOpenFileRef &&
144 | this.inputOpenFileRef.current
145 | ) {
146 | this.inputOpenFileRef.current.click()
147 | }
148 | }}
149 | >
150 |
151 |
152 |
153 | )
154 | }}
155 | fullWidth
156 | />
157 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------