Repository: luin/medis
Branch: master
Commit: 12c87a5a2cc3
Files: 92
Total size: 214.7 KB
Directory structure:
gitextract_a17rmpp5/
├── .babelrc
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin/
│ └── pack.js
├── package.json
├── resources/
│ ├── child.plist
│ ├── icns/
│ │ ├── MyIcon.icns
│ │ └── generate
│ └── parent.plist
├── src/
│ ├── main/
│ │ ├── index.ts
│ │ ├── menu.ts
│ │ └── windowManager.ts
│ └── renderer/
│ ├── images/
│ │ └── design.sketch
│ ├── photon/
│ │ └── css/
│ │ └── photon.css
│ ├── redux/
│ │ ├── actions/
│ │ │ ├── connection.js
│ │ │ ├── favorites.js
│ │ │ ├── index.js
│ │ │ ├── instances.js
│ │ │ ├── patterns.js
│ │ │ └── sizes.js
│ │ ├── middlewares/
│ │ │ ├── createThunkReplyMiddleware.js
│ │ │ └── index.js
│ │ ├── persistEnhancer.js
│ │ ├── reducers/
│ │ │ ├── activeInstanceKey.js
│ │ │ ├── favorites.js
│ │ │ ├── index.js
│ │ │ ├── instances.js
│ │ │ ├── patterns.js
│ │ │ └── sizes.js
│ │ └── store.js
│ ├── storage/
│ │ ├── Favorites.js
│ │ ├── Patterns.js
│ │ ├── Sizes.js
│ │ └── index.js
│ ├── styles/
│ │ ├── global.scss
│ │ ├── native.scss
│ │ └── photon.scss
│ ├── utils.ts
│ ├── vendors/
│ │ └── jquery.terminal/
│ │ └── index.css
│ └── windows/
│ ├── MainWindow/
│ │ ├── InstanceContent/
│ │ │ ├── ConnectionSelectorContainer/
│ │ │ │ ├── Config/
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── index.scss
│ │ │ │ ├── Favorite.jsx
│ │ │ │ └── index.jsx
│ │ │ ├── DatabaseContainer/
│ │ │ │ ├── AddButton/
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── index.scss
│ │ │ │ ├── Content/
│ │ │ │ │ ├── Config/
│ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ └── index.scss
│ │ │ │ │ ├── Footer.jsx
│ │ │ │ │ ├── KeyContent/
│ │ │ │ │ │ ├── BaseContent/
│ │ │ │ │ │ │ ├── Editor/
│ │ │ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ │ │ └── index.scss
│ │ │ │ │ │ │ ├── HashContent.jsx
│ │ │ │ │ │ │ ├── ListContent.jsx
│ │ │ │ │ │ │ ├── SetContent.jsx
│ │ │ │ │ │ │ ├── SortHeaderCell.jsx
│ │ │ │ │ │ │ ├── StringContent.jsx
│ │ │ │ │ │ │ ├── ZSetContent.jsx
│ │ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ │ └── index.scss
│ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ └── index.scss
│ │ │ │ │ ├── TabBar/
│ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ └── index.scss
│ │ │ │ │ ├── Terminal/
│ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ └── index.scss
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── ContentEditable/
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ └── index.scss
│ │ │ │ ├── KeyBrowser/
│ │ │ │ │ ├── Footer.jsx
│ │ │ │ │ ├── KeyList/
│ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ └── index.scss
│ │ │ │ │ ├── PatternList/
│ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ └── index.scss
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.scss
│ │ │ ├── Modal/
│ │ │ │ ├── index.jsx
│ │ │ │ └── index.scss
│ │ │ └── index.jsx
│ │ ├── InstanceTabs/
│ │ │ ├── Tab.tsx
│ │ │ ├── Tabs.tsx
│ │ │ ├── index.tsx
│ │ │ └── main.scss
│ │ ├── entry.jsx
│ │ └── index.jsx
│ └── PatternManagerWindow/
│ ├── app.scss
│ ├── entry.jsx
│ └── index.jsx
├── tsconfig.json
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"ignore": [
"buffer"
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-class-properties"
],
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "69"
}
}
],
"@babel/preset-react"
]
}
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [luin]
custom: https://apps.apple.com/us/app/medis-2-gui-for-redis/id1579200037
================================================
FILE: .gitignore
================================================
node_modules
.DS_Store
dist
npm-debug.log
*.provisionprofile
.awcache
================================================
FILE: CHANGELOG.md
================================================
## [0.6.1](https://github.com/luin/medis/compare/v0.5.0...v0.6.1) (2017-02-19)
### Bug Fixes
* detect database number for Heroku Redis ([f2c6d7e](https://github.com/luin/medis/commit/f2c6d7e)), closes [#55](https://github.com/luin/medis/issues/55) [#52](https://github.com/luin/medis/issues/52)
* UI for edit button ([3599392](https://github.com/luin/medis/commit/3599392))
* zset delete wrong element when sorting desc ([3d3f29a](https://github.com/luin/medis/commit/3d3f29a)), closes [#60](https://github.com/luin/medis/issues/60)
### Features
* support search/find within a key ([9ecce73](https://github.com/luin/medis/commit/9ecce73)), closes [#61](https://github.com/luin/medis/issues/61)
# [0.6.0](https://github.com/luin/medis/compare/v0.5.0...v0.6.0) (2017-02-19)
### Bug Fixes
* detect database number for Heroku Redis ([f2c6d7e](https://github.com/luin/medis/commit/f2c6d7e)), closes [#55](https://github.com/luin/medis/issues/55) [#52](https://github.com/luin/medis/issues/52)
* UI for edit button ([3599392](https://github.com/luin/medis/commit/3599392))
* zset delete wrong element when sorting desc ([3d3f29a](https://github.com/luin/medis/commit/3d3f29a)), closes [#60](https://github.com/luin/medis/issues/60)
### Features
* support search/find within a key ([9ecce73](https://github.com/luin/medis/commit/9ecce73)), closes [#61](https://github.com/luin/medis/issues/61)
# [0.5.0](https://github.com/luin/medis/compare/v0.3.0...v0.5.0) (2016-12-04)
### Bug Fixes
* check err first before update the database status ([dd46cc3](https://github.com/luin/medis/commit/dd46cc3))
* clear the state before leaving the favorite page ([498a077](https://github.com/luin/medis/commit/498a077))
* don't show error multiple times when lost connection to SSH tunnel ([2b732bd](https://github.com/luin/medis/commit/2b732bd))
* fix psubscribe not working. Close #32 ([586a943](https://github.com/luin/medis/commit/586a943)), closes [#32](https://github.com/luin/medis/issues/32)
* provide details error when connection is failed ([99d2757](https://github.com/luin/medis/commit/99d2757))
* tweak config panel style ([d92faf2](https://github.com/luin/medis/commit/d92faf2))
* ui issues when switching between tabs ([330f52f](https://github.com/luin/medis/commit/330f52f)), closes [#1](https://github.com/luin/medis/issues/1)
### Features
* add support for SSL connection. ([ca29384](https://github.com/luin/medis/commit/ca29384)), closes [#41](https://github.com/luin/medis/issues/41)
* allow quick connecting by double clicking ([53a284e](https://github.com/luin/medis/commit/53a284e))
* support Elastic Cache Redis & RedisLabs for selecting database ([18e5629](https://github.com/luin/medis/commit/18e5629))
* support to duplicate favorites ([c2bc438](https://github.com/luin/medis/commit/c2bc438)), closes [#30](https://github.com/luin/medis/issues/30)
* use Consolas font instead ([bd9d1c9](https://github.com/luin/medis/commit/bd9d1c9)), closes [#2](https://github.com/luin/medis/issues/2) [#39](https://github.com/luin/medis/issues/39)
# [0.5.0](https://github.com/luin/medis/compare/v0.3.0...v0.5.0) (2016-12-04)
### Bug Fixes
* check err first before update the database status ([dd46cc3](https://github.com/luin/medis/commit/dd46cc3))
* clear the state before leaving the favorite page ([498a077](https://github.com/luin/medis/commit/498a077))
* don't show error multiple times when lost connection to SSH tunnel ([2b732bd](https://github.com/luin/medis/commit/2b732bd))
* fix psubscribe not working. Close #32 ([586a943](https://github.com/luin/medis/commit/586a943)), closes [#32](https://github.com/luin/medis/issues/32)
* provide details error when connection is failed ([99d2757](https://github.com/luin/medis/commit/99d2757))
* tweak config panel style ([d92faf2](https://github.com/luin/medis/commit/d92faf2))
* ui issues when switching between tabs ([330f52f](https://github.com/luin/medis/commit/330f52f)), closes [#1](https://github.com/luin/medis/issues/1)
### Features
* add support for SSL connection. ([ca29384](https://github.com/luin/medis/commit/ca29384)), closes [#41](https://github.com/luin/medis/issues/41)
* allow quick connecting by double clicking ([53a284e](https://github.com/luin/medis/commit/53a284e))
* support Elastic Cache Redis & RedisLabs for selecting database ([18e5629](https://github.com/luin/medis/commit/18e5629))
* support to duplicate favorites ([c2bc438](https://github.com/luin/medis/commit/c2bc438)), closes [#30](https://github.com/luin/medis/issues/30)
* use Consolas font instead ([bd9d1c9](https://github.com/luin/medis/commit/bd9d1c9)), closes [#2](https://github.com/luin/medis/issues/2) [#39](https://github.com/luin/medis/issues/39)
# [0.3.0](https://github.com/luin/medis/compare/v0.2.1...v0.3.0) (2016-03-25)
### Bug Fixes
* **windows:** hide app menu in Windows version ([d31bd6c](https://github.com/luin/medis/commit/d31bd6c))
### Features
* support inputing spaces in terminal ([04e7bcf](https://github.com/luin/medis/commit/04e7bcf)), closes [#24](https://github.com/luin/medis/issues/24)
## [0.2.1](https://github.com/luin/medis/compare/v0.2.0...v0.2.1) (2016-02-01)
### Bug Fixes
* **ssh:** fix ssh password being ignored ([4dfdbcd](https://github.com/luin/medis/commit/4dfdbcd)), closes [#13](https://github.com/luin/medis/issues/13)
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2016-2022 Zihua Li
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Medis
### Notice: We just released Medis 2! 🚀🚀🚀
Compared to Medis (this repo), Medis 2 provides more delightful features, such as **tree view** (yes, finally!), streams, alert mode, **dark mode**, and more. Besides that, Medis 2 is rewritten from the beginning with native technology, making it more morden, beautiful, and fast!
What's more, **Medis 2 is free 💰 to download**! Don't hesitate, download it from the App Store now and try it out!
[](https://apps.apple.com/us/app/medis-2-gui-for-redis/id1579200037?mt=12)
_(or searching "Medis 2" on macOS App Store if the above link doesn't work for you. Also, you can download the app directly from the [official website](https://getmedis.com/))_

---
Medis is a beautiful, easy-to-use Redis management application built on the modern web with [Electron](https://github.com/atom/electron), [React](https://facebook.github.io/react/), and [Redux](https://github.com/rackt/redux). It's powered by many awesome Node.js modules, especially [ioredis](https://github.com/luin/ioredis) and [ssh2](https://github.com/mscdex/ssh2).
[](http://commitizen.github.io/cz-cli/)
Medis starts with all the basic features you need:
- Keys viewing/editing
- SSH Tunnel for connecting with remote servers
- Terminal for executing custom commands
- Config viewing/editing
It also supports many advanced features:
- JSON/MessagePack format viewing/editing and built-in highlighting/validator
- Working with millions keys and key members without blocking the redis server
- Pattern manager for easy selecting a sub group of keys.
**Note**: Medis only supports Redis >= 2.8 version because `SCAN` command was introduced since 2.8. `SCAN` is very useful to get key list without blocking the server, which is crucial to the production environment. Because the latest stable is 5.0 and 2.6 is a very old version, Medis doesn't support it.
## Download Medis on Windows
You can download compiled installer of Medis for Windows from the below page
[download page](https://github.com/classfellow/medis/releases/tag/win)
## Download Medis on Mac
You can download compiled versions of Medis for Mac OS X from [the release page](https://github.com/luin/medis/releases).
## Running Locally
1. Install dependencies
```
$ npm install
```
2. Compile assets:
```
$ npm run pack
```
3. Run with Electron:
```
$ npm start
```
## Connect to Heroku
Medis can connect to Heroku Redis addon to manage your data. You just need to call `heroku redis:credentials --app APP` to get your redis credential:
```shell
$ heroku redis:credentials --app YOUR_APP
redis://x:PASSWORD@HOST:PORT
```
And then input `HOST`, `PORT` and `PASSWORD` to the connection tab.
## I Love This. How do I Help?
- Simply star this repository :-)
- Help us spread the world on Facebook and Twitter
- Contribute Code! We're developers! (See Roadmap below)
- Medis is available on the Mac App Store as a paid software. I'll be very grateful if you'd like to buy it to encourage me to continue maintaining Medis. There are no additional features comparing with the open-sourced version, except the fact that you can enjoy auto updating that brought by the Mac App Store. [](https://apps.apple.com/us/app/medis-2-gui-for-redis/id1579200037?mt=12)
## Roadmap
- Windows and Linux version (with electron-packager)
- Support for SaaS Redis services
- Lua script editor
- Cluster management
- GEO keys supporting
## Contributors
luin
kvnsmth
dpde
ogasawaraShinnosuke
naholyr
hlobil
Janpot
## License
MIT
================================================
FILE: bin/pack.js
================================================
const packager = require('electron-packager')
const path = require('path')
const pkg = require('../package')
const flat = require('electron-osx-sign').flat
const resourcesPath = path.join(__dirname, '..', 'resources')
packager({
dir: path.join(__dirname, '..'),
appCopyright: '© 2019, Zihua Li',
asar: true,
overwrite: true,
electronVersion: pkg.electronVersion,
icon: path.join(resourcesPath, 'icns', 'MyIcon'),
out: path.join(__dirname, '..', 'dist', 'out'),
platform: 'mas',
appBundleId: `li.zihua.${pkg.name}`,
appCategoryType: 'public.app-category.developer-tools',
osxSign: {
type: process.env.NODE_ENV === 'production' ? 'distribution' : 'development',
entitlements: path.join(resourcesPath, 'parent.plist'),
'entitlements-inherit': path.join(resourcesPath, 'child.plist')
}
}).then((res) => {
const app = path.join(res[0], `${pkg.productName}.app`)
console.log('flating...', app)
flat({ app }, function done (err) {
if (err) {
throw err
}
process.exit(0);
})
})
================================================
FILE: package.json
================================================
{
"name": "medis",
"description": "GUI for Redis",
"productName": "Medis",
"version": "1.0.3",
"electronVersion": "4.0.0",
"license": "MIT",
"author": "luin (http://zihua.li)",
"main": "dist/main",
"scripts": {
"build": "rm -rf dist && webpack",
"watch": "WEBPACK_WATCH=true npm run build",
"start": "electron .",
"pack": "NODE_ENV=production npm run build && node bin/pack.js"
},
"repository": {
"type": "git",
"url": "git://github.com/luin/medis.git"
},
"dependencies": {
"electron": "^4.2.2",
"electron-context-menu": "^0.12.1",
"fixed-data-table-contextmenu": "^1.7.2",
"ioredis": "^4.9.3",
"jquery": "^3.4.1",
"jquery.terminal": "^2.5.1",
"lodash.escape": "^4.0.1",
"lodash.sortedindexby": "^4.6.0",
"medis-react-codemirror": "^1.1.1",
"redis-commands": "^1.5.0",
"ssh2": "^0.8.9",
"xterm": "^3.13.1"
},
"devDependencies": {
"@babel/core": "^7.4.4",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.4.4",
"@babel/preset-react": "^7.0.0",
"@types/jquery": "^3.3.29",
"@types/react": "^16.8.17",
"@types/react-dom": "^16.8.4",
"@types/redux-actions": "^2.6.1",
"awesome-typescript-loader": "^5.2.1",
"babel-loader": "^8.0.6",
"codemirror": "^5.46.0",
"css-loader": "^2.1.1",
"electron-osx-sign": "^0.4.4",
"electron-packager": "^13.1.1",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"human-format": "^0.10.1",
"immutable": "^3.8.1",
"json-editor": "^0.7.23",
"jsonlint": "^1.6.2",
"jsx-loader": "^0.13.2",
"lint": "^1.1.2",
"lodash.clone": "^4.5.0",
"lodash.zip": "^4.2.0",
"mini-css-extract-plugin": "^0.6.0",
"minimatch": "^3.0.4",
"msgpack5": "^4.2.1",
"node-sass": "^4.12.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
"react-addons-css-transition-group": "^15.5.2",
"react-document-title": "^2.0.1",
"react-dom": "^16.8.6",
"react-redux": "^7.0.3",
"react-sortable-hoc": "^1.9.1",
"react-split-pane": "^0.1.87",
"redis-splitargs": "^1.0.1",
"redux": "^4.0.1",
"redux-actions": "^2.6.5",
"reselect": "^4.0.0",
"sass-loader": "^7.1.0",
"sortablejs": "^1.9.0",
"typescript": "^3.4.5",
"url-loader": "^1.1.2",
"webpack": "^4.31.0",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-cli": "^3.3.2"
}
}
================================================
FILE: resources/child.plist
================================================
com.apple.security.app-sandbox
com.apple.security.inherit
================================================
FILE: resources/icns/generate
================================================
mkdir MyIcon.iconset
sips -z 16 16 Icon1024.png --out MyIcon.iconset/icon_16x16.png
sips -z 32 32 Icon1024.png --out MyIcon.iconset/icon_16x16@2x.png
sips -z 32 32 Icon1024.png --out MyIcon.iconset/icon_32x32.png
sips -z 64 64 Icon1024.png --out MyIcon.iconset/icon_32x32@2x.png
sips -z 128 128 Icon1024.png --out MyIcon.iconset/icon_128x128.png
sips -z 256 256 Icon1024.png --out MyIcon.iconset/icon_128x128@2x.png
sips -z 256 256 Icon1024.png --out MyIcon.iconset/icon_256x256.png
sips -z 512 512 Icon1024.png --out MyIcon.iconset/icon_256x256@2x.png
sips -z 512 512 Icon1024.png --out MyIcon.iconset/icon_512x512.png
cp Icon1024.png MyIcon.iconset/icon_512x512@2x.png
iconutil -c icns MyIcon.iconset
rm -R MyIcon.iconset
================================================
FILE: resources/parent.plist
================================================
com.apple.security.app-sandbox
com.apple.security.files.user-selected.read-only
com.apple.security.network.client
com.apple.security.network.server
================================================
FILE: src/main/index.ts
================================================
import {app, Menu, ipcMain} from 'electron'
import windowManager from './windowManager'
import menu from './menu'
const contextMenu = require('electron-context-menu');
contextMenu({
// showInspectElement: true,
})
ipcMain.on('create patternManager', function (event, arg) {
windowManager.create('patternManager', arg)
})
ipcMain.on('dispatch', function (event, action, arg) {
windowManager.dispatch(action, arg)
})
// Quit when all windows are closed.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
app.on('ready', function () {
Menu.setApplicationMenu(menu)
windowManager.create()
app.on('activate', function (_, hasVisibleWindows) {
if (!hasVisibleWindows) {
windowManager.create()
}
})
})
================================================
FILE: src/main/menu.ts
================================================
import {app, Menu, MenuItemConstructorOptions} from 'electron'
import windowManager from './windowManager'
const menuTemplates: MenuItemConstructorOptions[] = [{
label: 'File',
submenu: [{
label: 'New Connection Window',
accelerator: 'CmdOrCtrl+N',
click() {
windowManager.create()
}
}, {
label: 'New Connection Tab',
accelerator: 'CmdOrCtrl+T',
click() {
windowManager.current.webContents.send('action', 'createInstance')
}
}, {
type: 'separator'
}, {
label: 'Close Window',
accelerator: 'Shift+CmdOrCtrl+W',
click() {
windowManager.current.close()
}
}, {
label: 'Close Tab',
accelerator: 'CmdOrCtrl+W',
click() {
windowManager.current.webContents.send('action', 'delInstance')
}
}]
}, {
label: 'Edit',
submenu: [{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
role: 'undo'
}, {
label: 'Redo',
accelerator: 'Shift+CmdOrCtrl+Z',
role: 'redo'
}, {
type: 'separator'
}, {
label: 'Cut',
accelerator: 'CmdOrCtrl+X',
role: 'cut'
}, {
label: 'Copy',
accelerator: 'CmdOrCtrl+C',
role: 'copy'
}, {
label: 'Paste',
accelerator: 'CmdOrCtrl+V',
role: 'paste'
}, {
label: 'Select All',
accelerator: 'CmdOrCtrl+A',
role: 'selectall'
}]
}, {
label: 'View',
submenu: [{
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.reload()
}
}
}, {
label: 'Toggle Full Screen',
accelerator: (function () {
if (process.platform === 'darwin') {
return 'Ctrl+Command+F'
}
return 'F11'
})(),
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
}
}
}, {
label: 'Toggle Developer Tools',
accelerator: (function () {
if (process.platform === 'darwin') {
return 'Alt+Command+I'
}
return 'Ctrl+Shift+I'
})(),
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.webContents.toggleDevTools()
}
}
}]
}, {
label: 'Window',
role: 'window',
submenu: [{
label: 'Minimize',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}, {
label: 'Close',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}]
}, {
label: 'Help',
role: 'help',
submenu: [{
label: 'Report an Issue...',
click() {
require('shell').openExternal('mailto:medis@zihua.li')
}
}, {
label: 'Learn More',
click() {
require('shell').openExternal('http://getmedis.com')
}
}]
}]
let baseIndex = 0
if (process.platform == 'darwin') {
baseIndex = 1
menuTemplates.unshift({
label: app.getName(),
submenu: [{
label: 'About ' + app.getName(),
role: 'about'
}, {
type: 'separator'
}, {
label: 'Services',
role: 'services',
submenu: []
}, {
type: 'separator'
}, {
label: 'Hide ' + app.getName(),
accelerator: 'Command+H',
role: 'hide'
}, {
label: 'Hide Others',
accelerator: 'Command+Shift+H',
role: 'hideothers'
}, {
label: 'Show All',
role: 'unhide'
}, {
type: 'separator'
}, {
label: 'Quit',
accelerator: 'Command+Q',
click() {
app.quit()
}
}]
})
}
const menu = Menu.buildFromTemplate(menuTemplates)
if (process.env.NODE_ENV === 'production') {
const {submenu} = (menu.items[baseIndex + 2] as any)
submenu.items[0].visible = false
submenu.items[2].visible = false
}
const {submenu} = (menu.items[baseIndex + 0] as any)
windowManager.on('blur', function () {
submenu.items[3].enabled = false
submenu.items[4].enabled = false
})
windowManager.on('focus', function () {
const {submenu} = (menu.items[baseIndex + 0] as any)
submenu.items[3].enabled = true
submenu.items[4].enabled = true
})
export default menu
================================================
FILE: src/main/windowManager.ts
================================================
import {app, BrowserWindow, BrowserWindowConstructorOptions} from 'electron'
import path from 'path'
import EventEmitter from 'events'
class WindowManager extends EventEmitter {
windows = new Set()
constructor() {
super()
app.on('browser-window-blur', this.emit.bind(this, 'blur'))
app.on('browser-window-focus', this.emit.bind(this, 'focus'))
}
get current() {
return BrowserWindow.getFocusedWindow() || this.create()
}
create(type = 'main', arg?: any): BrowserWindow {
const option: BrowserWindowConstructorOptions = {
backgroundColor: '#ececec',
webPreferences: {
nodeIntegration: true
}
}
if (type === 'main') {
option.width = 960
option.height = 600
option.show = false
option.minWidth = 840
option.minHeight = 400
} else if (type === 'patternManager') {
option.width = 600
option.height = 300
option.title = 'Manage Patterns'
option.resizable = true
option.fullscreen = false
}
let start: number
const newWindow = new BrowserWindow(option)
if (!option.show) {
newWindow.once('ready-to-show', () => {
console.log('start time: ', Date.now() - start)
newWindow.show()
})
}
start = Date.now()
newWindow.loadFile(path.resolve(__dirname, `../renderer/${type}.html`), {query: {arg}})
this._register(newWindow)
return newWindow
}
_register(win: BrowserWindow): void {
this.windows.add(win)
win.on('closed', () => {
this.windows.delete(win)
if (!BrowserWindow.getFocusedWindow()) {
this.emit('blur')
}
})
this.emit('focus')
}
dispatch(action: string, args: any) {
this.windows.forEach(win => {
if (win && win.webContents) {
win.webContents.send('action', action, args)
}
})
}
}
export default new WindowManager()
================================================
FILE: src/renderer/photon/css/photon.css
================================================
/*!
* =====================================================
* Photon v0.1.0
* Copyright 2015 Connor Sears
* Licensed under MIT (https://github.com/connors/proton/blob/master/LICENSE)
*
* v0.1.0 designed by @connors.
* =====================================================
*/
@charset "UTF-8";
audio,
canvas,
progress,
video {
vertical-align: baseline;
}
audio:not([controls]) {
display: none;
}
a:active,
a:hover {
outline: 0;
}
abbr[title] {
border-bottom: 1px dotted;
}
b,
strong {
font-weight: bold;
}
dfn {
font-style: italic;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
pre {
overflow: auto;
}
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
button,
input,
optgroup,
select,
textarea {
color: inherit;
font: inherit;
margin: 0;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
input[type="search"] {
-webkit-appearance: textfield;
box-sizing: content-box;
}
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
border: 0;
padding: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}
* {
cursor: default;
-webkit-user-drag: text;
-webkit-user-select: none;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
html {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
height: 100%;
padding: 0;
margin: 0;
font-family: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif;
font-size: 13px;
line-height: 1.6;
color: #333;
background-color: transparent;
}
hr {
margin: 15px 0;
overflow: hidden;
background: transparent;
border: 0;
border-bottom: 1px solid #ddd;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 20px;
margin-bottom: 10px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
h1 {
font-size: 36px;
}
h2 {
font-size: 30px;
}
h3 {
font-size: 24px;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 14px;
}
h6 {
font-size: 12px;
}
.window {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
background-color: #ececec;
}
.window-content {
position: relative;
overflow-y: auto;
display: flex;
flex: 1;
}
.selectable-text {
cursor: text;
-webkit-user-select: text;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
.pull-left {
float: left;
}
.pull-right {
float: right;
}
.padded {
padding: 10px;
}
.padded-less {
padding: 5px;
}
.padded-more {
padding: 20px;
}
.padded-vertically {
padding-top: 10px;
padding-bottom: 10px;
}
.padded-vertically-less {
padding-top: 5px;
padding-bottom: 5px;
}
.padded-vertically-more {
padding-top: 20px;
padding-bottom: 20px;
}
.padded-horizontally {
padding-right: 10px;
padding-left: 10px;
}
.padded-horizontally-less {
padding-right: 5px;
padding-left: 5px;
}
.padded-horizontally-more {
padding-right: 20px;
padding-left: 20px;
}
.padded-top {
padding-top: 10px;
}
.padded-top-less {
padding-top: 5px;
}
.padded-top-more {
padding-top: 20px;
}
.padded-bottom {
padding-bottom: 10px;
}
.padded-bottom-less {
padding-bottom: 5px;
}
.padded-bottom-more {
padding-bottom: 20px;
}
.sidebar {
background-color: #f5f5f4;
}
.clearfix:before, .clearfix:after {
display: table;
content: " ";
}
.clearfix:after {
clear: both;
}
.btn {
display: inline-block;
padding: 3px 8px;
margin-bottom: 0;
font-size: 12px;
line-height: 1.4;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: default;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
-webkit-app-region: no-drag;
}
.btn:focus {
outline: none;
box-shadow: none;
}
.btn-mini {
padding: 2px 6px;
}
.btn-large {
padding: 6px 12px;
}
.btn-form {
padding-right: 20px;
padding-left: 20px;
}
.btn-default {
color: #333;
border-top-color: #c2c0c2;
border-right-color: #c2c0c2;
border-bottom-color: #a19fa1;
border-left-color: #c2c0c2;
background-color: #fcfcfc;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fcfcfc), color-stop(100%, #f1f1f1));
background-image: -webkit-linear-gradient(top, #fcfcfc 0%, #f1f1f1 100%);
background-image: linear-gradient(to bottom, #fcfcfc 0%, #f1f1f1 100%);
}
.btn-default:active {
background-color: #ddd;
background-image: none;
}
.btn-primary,
.btn-positive,
.btn-negative,
.btn-warning {
color: #fff;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
}
.btn-primary {
border-color: #388df8;
border-bottom-color: #0866dc;
background-color: #6eb4f7;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #6eb4f7), color-stop(100%, #1a82fb));
background-image: -webkit-linear-gradient(top, #6eb4f7 0%, #1a82fb 100%);
background-image: linear-gradient(to bottom, #6eb4f7 0%, #1a82fb 100%);
}
.btn-primary:active {
background-color: #3e9bf4;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3e9bf4), color-stop(100%, #0469de));
background-image: -webkit-linear-gradient(top, #3e9bf4 0%, #0469de 100%);
background-image: linear-gradient(to bottom, #3e9bf4 0%, #0469de 100%);
}
.btn-positive {
border-color: #29a03b;
border-bottom-color: #248b34;
background-color: #5bd46d;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bd46d), color-stop(100%, #29a03b));
background-image: -webkit-linear-gradient(top, #5bd46d 0%, #29a03b 100%);
background-image: linear-gradient(to bottom, #5bd46d 0%, #29a03b 100%);
}
.btn-positive:active {
background-color: #34c84a;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #34c84a), color-stop(100%, #248b34));
background-image: -webkit-linear-gradient(top, #34c84a 0%, #248b34 100%);
background-image: linear-gradient(to bottom, #34c84a 0%, #248b34 100%);
}
.btn-negative {
border-color: #fb2f29;
border-bottom-color: #fb1710;
background-color: #fd918d;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fd918d), color-stop(100%, #fb2f29));
background-image: -webkit-linear-gradient(top, #fd918d 0%, #fb2f29 100%);
background-image: linear-gradient(to bottom, #fd918d 0%, #fb2f29 100%);
}
.btn-negative:active {
background-color: #fc605b;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fc605b), color-stop(100%, #fb1710));
background-image: -webkit-linear-gradient(top, #fc605b 0%, #fb1710 100%);
background-image: linear-gradient(to bottom, #fc605b 0%, #fb1710 100%);
}
.btn-warning {
border-color: #fcaa0e;
border-bottom-color: #ee9d02;
background-color: #fece72;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fece72), color-stop(100%, #fcaa0e));
background-image: -webkit-linear-gradient(top, #fece72 0%, #fcaa0e 100%);
background-image: linear-gradient(to bottom, #fece72 0%, #fcaa0e 100%);
}
.btn-warning:active {
background-color: #fdbc40;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fdbc40), color-stop(100%, #ee9d02));
background-image: -webkit-linear-gradient(top, #fdbc40 0%, #ee9d02 100%);
background-image: linear-gradient(to bottom, #fdbc40 0%, #ee9d02 100%);
}
.btn .icon {
float: left;
width: 14px;
height: 14px;
margin-top: 1px;
margin-bottom: 1px;
color: #737475;
font-size: 14px;
line-height: 1;
}
.btn .icon-text {
margin-right: 5px;
}
.btn-dropdown:after {
font-family: "photon-entypo";
margin-left: 5px;
content: "";
}
.btn-group {
position: relative;
display: inline-block;
vertical-align: middle;
-webkit-app-region: no-drag;
}
.btn-group .btn {
position: relative;
float: left;
}
.btn-group .btn:focus, .btn-group .btn:active {
z-index: 2;
}
.btn-group .btn.active {
z-index: 3;
}
.btn-group .btn + .btn,
.btn-group .btn + .btn-group,
.btn-group .btn-group + .btn,
.btn-group .btn-group + .btn-group {
margin-left: -1px;
}
.btn-group > .btn:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group > .btn:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.btn-group > .btn:not(:first-child):not(:last-child) {
border-radius: 0;
}
.btn-group .btn + .btn {
border-left: 1px solid #c2c0c2;
}
.btn-group .btn + .btn.active {
border-left: 0;
}
.btn-group .active {
color: #fff;
border: 1px solid transparent;
background-color: #6d6c6d;
background-image: none;
}
.btn-group .active .icon {
color: #fff;
}
.toolbar {
min-height: 22px;
box-shadow: inset 0 1px 0 #f5f4f5;
background-color: #e8e6e8;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #e8e6e8), color-stop(100%, #d1cfd1));
background-image: -webkit-linear-gradient(top, #e8e6e8 0%, #d1cfd1 100%);
background-image: linear-gradient(to bottom, #e8e6e8 0%, #d1cfd1 100%);
}
.toolbar:before, .toolbar:after {
display: table;
content: " ";
}
.toolbar:after {
clear: both;
}
.toolbar-header {
border-bottom: 1px solid #c2c0c2;
}
.toolbar-header .title {
margin-top: 1px;
}
.toolbar-footer {
border-top: 1px solid #c2c0c2;
-webkit-app-region: drag;
}
.toolbar-dark {
box-shadow: none;
background-color: #57acf5;
}
.toolbar-dark .title {
text-shadow: none;
color: #fff;
}
.title {
margin: 0;
font-size: 12px;
font-weight: 400;
text-align: center;
color: #555;
cursor: default;
}
.toolbar-borderless {
border-top: 0;
border-bottom: 0;
}
.toolbar-actions {
margin-top: 4px;
margin-bottom: 3px;
padding-right: 3px;
padding-left: 3px;
padding-bottom: 3px;
-webkit-app-region: drag;
}
.toolbar-actions:before, .toolbar-actions:after {
display: table;
content: " ";
}
.toolbar-actions:after {
clear: both;
}
.toolbar-actions > .btn,
.toolbar-actions > .btn-group {
margin-left: 4px;
margin-right: 4px;
}
label {
display: inline-block;
font-size: 13px;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
input[type="search"] {
box-sizing: border-box;
}
input[type="radio"],
input[type="checkbox"] {
margin: 4px 0 0;
line-height: normal;
}
.form-control {
display: block;
width: 100%;
min-height: 25px;
padding: 5px 10px;
font-size: 13px;
line-height: 1.6;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
outline: none;
}
.form-control:focus {
border-color: #6db3fd;
box-shadow: 3px 3px 0 #6db3fd, -3px -3px 0 #6db3fd, -3px 3px 0 #6db3fd, 3px -3px 0 #6db3fd;
}
textarea {
height: auto;
}
.form-group {
margin-bottom: 10px;
}
.radio,
.checkbox {
position: relative;
display: block;
margin-top: 10px;
margin-bottom: 10px;
}
.radio label,
.checkbox label {
padding-left: 20px;
margin-bottom: 0;
font-weight: normal;
}
.radio input[type="radio"],
.radio-inline input[type="radio"],
.checkbox input[type="checkbox"],
.checkbox-inline input[type="checkbox"] {
position: absolute;
margin-left: -20px;
margin-top: 4px;
}
.form-actions .btn {
margin-right: 10px;
}
.form-actions .btn:last-child {
margin-right: 0;
}
.pane-group {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
}
.pane {
position: relative;
overflow-y: auto;
flex: 1;
border-left: 1px solid #ddd;
}
.pane:first-child {
border-left: 0;
}
.pane-sm {
max-width: 220px;
min-width: 150px;
}
.pane-mini {
width: 80px;
flex: none;
}
.pane-one-fourth {
width: 25%;
flex: none;
}
.pane-one-third {
width: 33.3%;
}
img {
-webkit-user-drag: text;
}
.img-circle {
border-radius: 50%;
}
.img-rounded {
border-radius: 4px;
}
.list-group {
width: 100%;
list-style: none;
margin: 0;
padding: 0;
}
.list-group * {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.list-group-item {
padding: 10px;
font-size: 12px;
color: #414142;
border-top: 1px solid #ddd;
}
.list-group-item:first-child {
border-top: 0;
}
.list-group-item:active, .list-group-item.selected {
color: #fff;
background-color: #116cd6;
}
.list-group-header {
padding: 10px;
}
.media-object {
margin-top: 3px;
}
.media-object.pull-left {
margin-right: 10px;
}
.media-object.pull-right {
margin-left: 10px;
}
.media-body {
overflow: hidden;
}
.nav-group {
font-size: 14px;
}
.nav-group-item {
padding: 2px 10px 2px 25px;
display: block;
color: #333;
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-group-item:active, .nav-group-item.active {
background-color: #dcdfe1;
}
.nav-group-item .icon {
width: 19px;
height: 18px;
float: left;
color: #737475;
margin-top: -3px;
margin-right: 7px;
font-size: 18px;
text-align: center;
}
.nav-group-title {
margin: 0;
padding: 10px 10px 2px;
font-size: 12px;
font-weight: 500;
color: #666666;
}
@font-face {
font-family: "photon-entypo";
src: url("../fonts/photon-entypo.eot");
src: url("../fonts/photon-entypo.eot?#iefix") format("eot"), url("../fonts/photon-entypo.woff") format("woff"), url("../fonts/photon-entypo.ttf") format("truetype");
font-weight: normal;
font-style: normal;
}
.icon:before {
position: relative;
display: inline-block;
font-family: "photon-entypo";
speak: none;
font-size: 100%;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-note:before {
content: '\e800';
}
/* '' */
.icon-note-beamed:before {
content: '\e801';
}
/* '' */
.icon-music:before {
content: '\e802';
}
/* '' */
.icon-search:before {
content: '\e803';
}
/* '' */
.icon-flashlight:before {
content: '\e804';
}
/* '' */
.icon-mail:before {
content: '\e805';
}
/* '' */
.icon-heart:before {
content: '\e806';
}
/* '' */
.icon-heart-empty:before {
content: '\e807';
}
/* '' */
.icon-star:before {
content: '\e808';
}
/* '' */
.icon-star-empty:before {
content: '\e809';
}
/* '' */
.icon-user:before {
content: '\e80a';
}
/* '' */
.icon-users:before {
content: '\e80b';
}
/* '' */
.icon-user-add:before {
content: '\e80c';
}
/* '' */
.icon-video:before {
content: '\e80d';
}
/* '' */
.icon-picture:before {
content: '\e80e';
}
/* '' */
.icon-camera:before {
content: '\e80f';
}
/* '' */
.icon-layout:before {
content: '\e810';
}
/* '' */
.icon-menu:before {
content: '\e811';
}
/* '' */
.icon-check:before {
content: '\e812';
}
/* '' */
.icon-cancel:before {
content: '\e813';
}
/* '' */
.icon-cancel-circled:before {
content: '\e814';
}
/* '' */
.icon-cancel-squared:before {
content: '\e815';
}
/* '' */
.icon-plus:before {
content: '\e816';
}
/* '' */
.icon-plus-circled:before {
content: '\e817';
}
/* '' */
.icon-plus-squared:before {
content: '\e818';
}
/* '' */
.icon-minus:before {
content: '\e819';
}
/* '' */
.icon-minus-circled:before {
content: '\e81a';
}
/* '' */
.icon-minus-squared:before {
content: '\e81b';
}
/* '' */
.icon-help:before {
content: '\e81c';
}
/* '' */
.icon-help-circled:before {
content: '\e81d';
}
/* '' */
.icon-info:before {
content: '\e81e';
}
/* '' */
.icon-info-circled:before {
content: '\e81f';
}
/* '' */
.icon-back:before {
content: '\e820';
}
/* '' */
.icon-home:before {
content: '\e821';
}
/* '' */
.icon-link:before {
content: '\e822';
}
/* '' */
.icon-attach:before {
content: '\e823';
}
/* '' */
.icon-lock:before {
content: '\e824';
}
/* '' */
.icon-lock-open:before {
content: '\e825';
}
/* '' */
.icon-eye:before {
content: '\e826';
}
/* '' */
.icon-tag:before {
content: '\e827';
}
/* '' */
.icon-bookmark:before {
content: '\e828';
}
/* '' */
.icon-bookmarks:before {
content: '\e829';
}
/* '' */
.icon-flag:before {
content: '\e82a';
}
/* '' */
.icon-thumbs-up:before {
content: '\e82b';
}
/* '' */
.icon-thumbs-down:before {
content: '\e82c';
}
/* '' */
.icon-download:before {
content: '\e82d';
}
/* '' */
.icon-upload:before {
content: '\e82e';
}
/* '' */
.icon-upload-cloud:before {
content: '\e82f';
}
/* '' */
.icon-reply:before {
content: '\e830';
}
/* '' */
.icon-reply-all:before {
content: '\e831';
}
/* '' */
.icon-forward:before {
content: '\e832';
}
/* '' */
.icon-quote:before {
content: '\e833';
}
/* '' */
.icon-code:before {
content: '\e834';
}
/* '' */
.icon-export:before {
content: '\e835';
}
/* '' */
.icon-pencil:before {
content: '\e836';
}
/* '' */
.icon-feather:before {
content: '\e837';
}
/* '' */
.icon-print:before {
content: '\e838';
}
/* '' */
.icon-retweet:before {
content: '\e839';
}
/* '' */
.icon-keyboard:before {
content: '\e83a';
}
/* '' */
.icon-comment:before {
content: '\e83b';
}
/* '' */
.icon-chat:before {
content: '\e83c';
}
/* '' */
.icon-bell:before {
content: '\e83d';
}
/* '' */
.icon-attention:before {
content: '\e83e';
}
/* '' */
.icon-alert:before {
content: '\e83f';
}
/* '' */
.icon-vcard:before {
content: '\e840';
}
/* '' */
.icon-address:before {
content: '\e841';
}
/* '' */
.icon-location:before {
content: '\e842';
}
/* '' */
.icon-map:before {
content: '\e843';
}
/* '' */
.icon-direction:before {
content: '\e844';
}
/* '' */
.icon-compass:before {
content: '\e845';
}
/* '' */
.icon-cup:before {
content: '\e846';
}
/* '' */
.icon-trash:before {
content: '\e847';
}
/* '' */
.icon-doc:before {
content: '\e848';
}
/* '' */
.icon-docs:before {
content: '\e849';
}
/* '' */
.icon-doc-landscape:before {
content: '\e84a';
}
/* '' */
.icon-doc-text:before {
content: '\e84b';
}
/* '' */
.icon-doc-text-inv:before {
content: '\e84c';
}
/* '' */
.icon-newspaper:before {
content: '\e84d';
}
/* '' */
.icon-book-open:before {
content: '\e84e';
}
/* '' */
.icon-book:before {
content: '\e84f';
}
/* '' */
.icon-folder:before {
content: '\e850';
}
/* '' */
.icon-archive:before {
content: '\e851';
}
/* '' */
.icon-box:before {
content: '\e852';
}
/* '' */
.icon-rss:before {
content: '\e853';
}
/* '' */
.icon-phone:before {
content: '\e854';
}
/* '' */
.icon-cog:before {
content: '\e855';
}
/* '' */
.icon-tools:before {
content: '\e856';
}
/* '' */
.icon-share:before {
content: '\e857';
}
/* '' */
.icon-shareable:before {
content: '\e858';
}
/* '' */
.icon-basket:before {
content: '\e859';
}
/* '' */
.icon-bag:before {
content: '\e85a';
}
/* '' */
.icon-calendar:before {
content: '\e85b';
}
/* '' */
.icon-login:before {
content: '\e85c';
}
/* '' */
.icon-logout:before {
content: '\e85d';
}
/* '' */
.icon-mic:before {
content: '\e85e';
}
/* '' */
.icon-mute:before {
content: '\e85f';
}
/* '' */
.icon-sound:before {
content: '\e860';
}
/* '' */
.icon-volume:before {
content: '\e861';
}
/* '' */
.icon-clock:before {
content: '\e862';
}
/* '' */
.icon-hourglass:before {
content: '\e863';
}
/* '' */
.icon-lamp:before {
content: '\e864';
}
/* '' */
.icon-light-down:before {
content: '\e865';
}
/* '' */
.icon-light-up:before {
content: '\e866';
}
/* '' */
.icon-adjust:before {
content: '\e867';
}
/* '' */
.icon-block:before {
content: '\e868';
}
/* '' */
.icon-resize-full:before {
content: '\e869';
}
/* '' */
.icon-resize-small:before {
content: '\e86a';
}
/* '' */
.icon-popup:before {
content: '\e86b';
}
/* '' */
.icon-publish:before {
content: '\e86c';
}
/* '' */
.icon-window:before {
content: '\e86d';
}
/* '' */
.icon-arrow-combo:before {
content: '\e86e';
}
/* '' */
.icon-down-circled:before {
content: '\e86f';
}
/* '' */
.icon-left-circled:before {
content: '\e870';
}
/* '' */
.icon-right-circled:before {
content: '\e871';
}
/* '' */
.icon-up-circled:before {
content: '\e872';
}
/* '' */
.icon-down-open:before {
content: '\e873';
}
/* '' */
.icon-left-open:before {
content: '\e874';
}
/* '' */
.icon-right-open:before {
content: '\e875';
}
/* '' */
.icon-up-open:before {
content: '\e876';
}
/* '' */
.icon-down-open-mini:before {
content: '\e877';
}
/* '' */
.icon-left-open-mini:before {
content: '\e878';
}
/* '' */
.icon-right-open-mini:before {
content: '\e879';
}
/* '' */
.icon-up-open-mini:before {
content: '\e87a';
}
/* '' */
.icon-down-open-big:before {
content: '\e87b';
}
/* '' */
.icon-left-open-big:before {
content: '\e87c';
}
/* '' */
.icon-right-open-big:before {
content: '\e87d';
}
/* '' */
.icon-up-open-big:before {
content: '\e87e';
}
/* '' */
.icon-down:before {
content: '\e87f';
}
/* '' */
.icon-left:before {
content: '\e880';
}
/* '' */
.icon-right:before {
content: '\e881';
}
/* '' */
.icon-up:before {
content: '\e882';
}
/* '' */
.icon-down-dir:before {
content: '\e883';
}
/* '' */
.icon-left-dir:before {
content: '\e884';
}
/* '' */
.icon-right-dir:before {
content: '\e885';
}
/* '' */
.icon-up-dir:before {
content: '\e886';
}
/* '' */
.icon-down-bold:before {
content: '\e887';
}
/* '' */
.icon-left-bold:before {
content: '\e888';
}
/* '' */
.icon-right-bold:before {
content: '\e889';
}
/* '' */
.icon-up-bold:before {
content: '\e88a';
}
/* '' */
.icon-down-thin:before {
content: '\e88b';
}
/* '' */
.icon-left-thin:before {
content: '\e88c';
}
/* '' */
.icon-right-thin:before {
content: '\e88d';
}
/* '' */
.icon-up-thin:before {
content: '\e88e';
}
/* '' */
.icon-ccw:before {
content: '\e88f';
}
/* '' */
.icon-cw:before {
content: '\e890';
}
/* '' */
.icon-arrows-ccw:before {
content: '\e891';
}
/* '' */
.icon-level-down:before {
content: '\e892';
}
/* '' */
.icon-level-up:before {
content: '\e893';
}
/* '' */
.icon-shuffle:before {
content: '\e894';
}
/* '' */
.icon-loop:before {
content: '\e895';
}
/* '' */
.icon-switch:before {
content: '\e896';
}
/* '' */
.icon-play:before {
content: '\e897';
}
/* '' */
.icon-stop:before {
content: '\e898';
}
/* '' */
.icon-pause:before {
content: '\e899';
}
/* '' */
.icon-record:before {
content: '\e89a';
}
/* '' */
.icon-to-end:before {
content: '\e89b';
}
/* '' */
.icon-to-start:before {
content: '\e89c';
}
/* '' */
.icon-fast-forward:before {
content: '\e89d';
}
/* '' */
.icon-fast-backward:before {
content: '\e89e';
}
/* '' */
.icon-progress-0:before {
content: '\e89f';
}
/* '' */
.icon-progress-1:before {
content: '\e8a0';
}
/* '' */
.icon-progress-2:before {
content: '\e8a1';
}
/* '' */
.icon-progress-3:before {
content: '\e8a2';
}
/* '' */
.icon-target:before {
content: '\e8a3';
}
/* '' */
.icon-palette:before {
content: '\e8a4';
}
/* '' */
.icon-list:before {
content: '\e8a5';
}
/* '' */
.icon-list-add:before {
content: '\e8a6';
}
/* '' */
.icon-signal:before {
content: '\e8a7';
}
/* '' */
.icon-trophy:before {
content: '\e8a8';
}
/* '' */
.icon-battery:before {
content: '\e8a9';
}
/* '' */
.icon-back-in-time:before {
content: '\e8aa';
}
/* '' */
.icon-monitor:before {
content: '\e8ab';
}
/* '' */
.icon-mobile:before {
content: '\e8ac';
}
/* '' */
.icon-network:before {
content: '\e8ad';
}
/* '' */
.icon-cd:before {
content: '\e8ae';
}
/* '' */
.icon-inbox:before {
content: '\e8af';
}
/* '' */
.icon-install:before {
content: '\e8b0';
}
/* '' */
.icon-globe:before {
content: '\e8b1';
}
/* '' */
.icon-cloud:before {
content: '\e8b2';
}
/* '' */
.icon-cloud-thunder:before {
content: '\e8b3';
}
/* '' */
.icon-flash:before {
content: '\e8b4';
}
/* '' */
.icon-moon:before {
content: '\e8b5';
}
/* '' */
.icon-flight:before {
content: '\e8b6';
}
/* '' */
.icon-paper-plane:before {
content: '\e8b7';
}
/* '' */
.icon-leaf:before {
content: '\e8b8';
}
/* '' */
.icon-lifebuoy:before {
content: '\e8b9';
}
/* '' */
.icon-mouse:before {
content: '\e8ba';
}
/* '' */
.icon-briefcase:before {
content: '\e8bb';
}
/* '' */
.icon-suitcase:before {
content: '\e8bc';
}
/* '' */
.icon-dot:before {
content: '\e8bd';
}
/* '' */
.icon-dot-2:before {
content: '\e8be';
}
/* '' */
.icon-dot-3:before {
content: '\e8bf';
}
/* '' */
.icon-brush:before {
content: '\e8c0';
}
/* '' */
.icon-magnet:before {
content: '\e8c1';
}
/* '' */
.icon-infinity:before {
content: '\e8c2';
}
/* '' */
.icon-erase:before {
content: '\e8c3';
}
/* '' */
.icon-chart-pie:before {
content: '\e8c4';
}
/* '' */
.icon-chart-line:before {
content: '\e8c5';
}
/* '' */
.icon-chart-bar:before {
content: '\e8c6';
}
/* '' */
.icon-chart-area:before {
content: '\e8c7';
}
/* '' */
.icon-tape:before {
content: '\e8c8';
}
/* '' */
.icon-graduation-cap:before {
content: '\e8c9';
}
/* '' */
.icon-language:before {
content: '\e8ca';
}
/* '' */
.icon-ticket:before {
content: '\e8cb';
}
/* '' */
.icon-water:before {
content: '\e8cc';
}
/* '' */
.icon-droplet:before {
content: '\e8cd';
}
/* '' */
.icon-air:before {
content: '\e8ce';
}
/* '' */
.icon-credit-card:before {
content: '\e8cf';
}
/* '' */
.icon-floppy:before {
content: '\e8d0';
}
/* '' */
.icon-clipboard:before {
content: '\e8d1';
}
/* '' */
.icon-megaphone:before {
content: '\e8d2';
}
/* '' */
.icon-database:before {
content: '\e8d3';
}
/* '' */
.icon-drive:before {
content: '\e8d4';
}
/* '' */
.icon-bucket:before {
content: '\e8d5';
}
/* '' */
.icon-thermometer:before {
content: '\e8d6';
}
/* '' */
.icon-key:before {
content: '\e8d7';
}
/* '' */
.icon-flow-cascade:before {
content: '\e8d8';
}
/* '' */
.icon-flow-branch:before {
content: '\e8d9';
}
/* '' */
.icon-flow-tree:before {
content: '\e8da';
}
/* '' */
.icon-flow-line:before {
content: '\e8db';
}
/* '' */
.icon-flow-parallel:before {
content: '\e8dc';
}
/* '' */
.icon-rocket:before {
content: '\e8dd';
}
/* '' */
.icon-gauge:before {
content: '\e8de';
}
/* '' */
.icon-traffic-cone:before {
content: '\e8df';
}
/* '' */
.icon-cc:before {
content: '\e8e0';
}
/* '' */
.icon-cc-by:before {
content: '\e8e1';
}
/* '' */
.icon-cc-nc:before {
content: '\e8e2';
}
/* '' */
.icon-cc-nc-eu:before {
content: '\e8e3';
}
/* '' */
.icon-cc-nc-jp:before {
content: '\e8e4';
}
/* '' */
.icon-cc-sa:before {
content: '\e8e5';
}
/* '' */
.icon-cc-nd:before {
content: '\e8e6';
}
/* '' */
.icon-cc-pd:before {
content: '\e8e7';
}
/* '' */
.icon-cc-zero:before {
content: '\e8e8';
}
/* '' */
.icon-cc-share:before {
content: '\e8e9';
}
/* '' */
.icon-cc-remix:before {
content: '\e8ea';
}
/* '' */
.icon-github:before {
content: '\e8eb';
}
/* '' */
.icon-github-circled:before {
content: '\e8ec';
}
/* '' */
.icon-flickr:before {
content: '\e8ed';
}
/* '' */
.icon-flickr-circled:before {
content: '\e8ee';
}
/* '' */
.icon-vimeo:before {
content: '\e8ef';
}
/* '' */
.icon-vimeo-circled:before {
content: '\e8f0';
}
/* '' */
.icon-twitter:before {
content: '\e8f1';
}
/* '' */
.icon-twitter-circled:before {
content: '\e8f2';
}
/* '' */
.icon-facebook:before {
content: '\e8f3';
}
/* '' */
.icon-facebook-circled:before {
content: '\e8f4';
}
/* '' */
.icon-facebook-squared:before {
content: '\e8f5';
}
/* '' */
.icon-gplus:before {
content: '\e8f6';
}
/* '' */
.icon-gplus-circled:before {
content: '\e8f7';
}
/* '' */
.icon-pinterest:before {
content: '\e8f8';
}
/* '' */
.icon-pinterest-circled:before {
content: '\e8f9';
}
/* '' */
.icon-tumblr:before {
content: '\e8fa';
}
/* '' */
.icon-tumblr-circled:before {
content: '\e8fb';
}
/* '' */
.icon-linkedin:before {
content: '\e8fc';
}
/* '' */
.icon-linkedin-circled:before {
content: '\e8fd';
}
/* '' */
.icon-dribbble:before {
content: '\e8fe';
}
/* '' */
.icon-dribbble-circled:before {
content: '\e8ff';
}
/* '' */
.icon-stumbleupon:before {
content: '\e900';
}
/* '' */
.icon-stumbleupon-circled:before {
content: '\e901';
}
/* '' */
.icon-lastfm:before {
content: '\e902';
}
/* '' */
.icon-lastfm-circled:before {
content: '\e903';
}
/* '' */
.icon-rdio:before {
content: '\e904';
}
/* '' */
.icon-rdio-circled:before {
content: '\e905';
}
/* '' */
.icon-spotify:before {
content: '\e906';
}
/* '' */
.icon-spotify-circled:before {
content: '\e907';
}
/* '' */
.icon-qq:before {
content: '\e908';
}
/* '' */
.icon-instagram:before {
content: '\e909';
}
/* '' */
.icon-dropbox:before {
content: '\e90a';
}
/* '' */
.icon-evernote:before {
content: '\e90b';
}
/* '' */
.icon-flattr:before {
content: '\e90c';
}
/* '' */
.icon-skype:before {
content: '\e90d';
}
/* '' */
.icon-skype-circled:before {
content: '\e90e';
}
/* '' */
.icon-renren:before {
content: '\e90f';
}
/* '' */
.icon-sina-weibo:before {
content: '\e910';
}
/* '' */
.icon-paypal:before {
content: '\e911';
}
/* '' */
.icon-picasa:before {
content: '\e912';
}
/* '' */
.icon-soundcloud:before {
content: '\e913';
}
/* '' */
.icon-mixi:before {
content: '\e914';
}
/* '' */
.icon-behance:before {
content: '\e915';
}
/* '' */
.icon-google-circles:before {
content: '\e916';
}
/* '' */
.icon-vkontakte:before {
content: '\e917';
}
/* '' */
.icon-smashing:before {
content: '\e918';
}
/* '' */
.icon-sweden:before {
content: '\e919';
}
/* '' */
.icon-db-shape:before {
content: '\e91a';
}
/* '' */
.icon-logo-db:before {
content: '\e91b';
}
/* '' */
table {
width: 100%;
border: 0;
border-collapse: separate;
font-size: 12px;
text-align: left;
}
thead {
background-color: #f5f5f4;
}
tbody {
background-color: #fff;
}
.table-striped tr:nth-child(even) {
background-color: #f5f5f4;
}
tr:active,
.table-striped tr:active:nth-child(even) {
color: #fff;
background-color: #116cd6;
}
thead tr:active {
color: #333;
background-color: #f5f5f4;
}
th {
font-weight: normal;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
th,
td {
padding: 2px 15px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
th:last-child,
td:last-child {
border-right: 0;
}
.tab-group {
margin-top: -1px;
display: flex;
border-top: 1px solid #989698;
border-bottom: 1px solid #989698;
}
.tab-item {
position: relative;
flex: 1;
padding: 3px;
font-size: 12px;
text-align: center;
border-left: 1px solid #989698;
background-color: #b8b6b8;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #b8b6b8), color-stop(100%, #b0aeb0));
background-image: -webkit-linear-gradient(top, #b8b6b8 0%, #b0aeb0 100%);
background-image: linear-gradient(to bottom, #b8b6b8 0%, #b0aeb0 100%);
}
.tab-item:first-child {
border-left: 0;
}
.tab-item.active {
background-color: #d4d2d4;
background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #d4d2d4), color-stop(100%, #cccacc));
background-image: -webkit-linear-gradient(top, #d4d2d4 0%, #cccacc 100%);
background-image: linear-gradient(to bottom, #d4d2d4 0%, #cccacc 100%);
}
.tab-item .icon-close-tab {
position: absolute;
top: 50%;
left: 5px;
width: 15px;
height: 15px;
font-size: 15px;
line-height: 15px;
text-align: center;
color: #666;
opacity: 0;
transition: opacity .1s linear, background-color .1s linear;
border-radius: 3px;
transform: translateY(-50%);
z-index: 10;
}
.tab-item:after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
content: "";
background-color: rgba(0, 0, 0, 0.08);
opacity: 0;
transition: opacity .1s linear;
z-index: 1;
}
.tab-item:hover:not(.active):after {
opacity: 1;
}
.tab-item:hover .icon-close-tab {
opacity: 1;
}
.tab-item .icon-close-tab:hover {
background-color: rgba(0, 0, 0, 0.08);
}
================================================
FILE: src/renderer/redux/actions/connection.js
================================================
'use strict';
import {createAction} from 'Utils';
import {Client} from 'ssh2';
import net from 'net';
import Redis from 'ioredis';
function getIndex(getState) {
const {activeInstanceKey, instances} = getState()
return instances.findIndex(instance => instance.get('key') === activeInstanceKey)
}
export const updateConnectStatus = createAction('UPDATE_CONNECT_STATUS', status => ({getState, next}) => {
next({status, index: getIndex(getState)})
})
export const disconnect = createAction('DISCONNECT', () => ({getState, next}) => {
next({index: getIndex(getState)})
})
export const connectToRedis = createAction('CONNECT', config => ({getState, dispatch, next}) => {
let sshErrorThrown = false
let redisErrorMessage
if (config.ssh) {
dispatch(updateConnectStatus('SSH connecting...'))
const conn = new Client();
conn.on('ready', () => {
const server = net.createServer(function (sock) {
conn.forwardOut(sock.remoteAddress, sock.remotePort, config.host, config.port, (err, stream) => {
if (err) {
sock.end()
} else {
sock.pipe(stream).pipe(sock)
}
})
}).listen(0, function () {
handleRedis(config, { host: '127.0.0.1', port: server.address().port })
})
}).on('error', err => {
sshErrorThrown = true;
dispatch(disconnect());
alert(`SSH Error: ${err.message}`);
})
try {
const connectionConfig = {
host: config.sshHost,
port: config.sshPort || 22,
username: config.sshUser
}
if (config.sshKey) {
conn.connect(Object.assign(connectionConfig, {
privateKey: config.sshKey,
passphrase: config.sshKeyPassphrase
}))
} else {
conn.connect(Object.assign(connectionConfig, {
password: config.sshPassword
}))
}
} catch (err) {
dispatch(disconnect());
alert(`SSH Error: ${err.message}`);
}
} else {
handleRedis(config);
}
function handleRedis(config, override) {
dispatch(updateConnectStatus('Redis connecting...'))
if (config.ssl) {
config.tls = {
rejectUnauthorized: false
};
if (config.tlsca) config.tls.ca = config.tlsca;
if (config.tlskey) config.tls.key = config.tlskey;
if (config.tlscert) config.tls.cert = config.tlscert;
}
const redis = new Redis(Object.assign({}, config, override, {
retryStrategy() {
return false;
}
}));
redis.defineCommand('setKeepTTL', {
numberOfKeys: 1,
lua: 'local ttl = redis.call("pttl", KEYS[1]) if ttl > 0 then return redis.call("SET", KEYS[1], ARGV[1], "PX", ttl) else return redis.call("SET", KEYS[1], ARGV[1]) end'
});
redis.defineCommand('lremindex', {
numberOfKeys: 1,
lua: 'local FLAG = "$$#__@DELETE@_REDIS_@PRO@__#$$" redis.call("lset", KEYS[1], ARGV[1], FLAG) redis.call("lrem", KEYS[1], 1, FLAG)'
});
redis.defineCommand('duplicateKey', {
numberOfKeys: 2,
lua: 'local dump = redis.call("dump", KEYS[1]) local pttl = 0 if ARGV[1] == "TTL" then pttl = redis.call("pttl", KEYS[1]) end return redis.call("restore", KEYS[2], pttl, dump)'
});
redis.once('connect', function () {
redis.ping((err, res) => {
if (err) {
if (err.message === 'Ready check failed: NOAUTH Authentication required.') {
err.message = 'Redis Error: Access denied. Please double-check your password.';
}
if (err.message !== 'Connection is closed.') {
alert(err.message);
redis.disconnect();
}
return;
}
const version = redis.serverInfo.redis_version;
if (version && version.length >= 5) {
const versionNumber = Number(version[0] + version[2]);
if (versionNumber < 28) {
alert('Medis only supports Redis >= 2.8 because servers older than 2.8 don\'t support SCAN command, which means it not possible to access keys without blocking Redis.');
dispatch(disconnect());
return;
}
}
next({redis, config, index: getIndex(getState)});
})
});
redis.once('error', function (error) {
redisErrorMessage = error;
});
redis.once('end', function () {
dispatch(disconnect());
if (!sshErrorThrown) {
let msg = 'Redis Error: Connection failed. ';
if (redisErrorMessage) {
msg += `(${redisErrorMessage})`;
}
alert(msg);
}
});
}
})
================================================
FILE: src/renderer/redux/actions/favorites.js
================================================
import {createAction} from 'Utils';
import {fromJS} from 'immutable'
import {Favorites} from '../../storage'
export const createFavorite = createAction('CREATE_FAVORITE', (data) => {
const key = `favorite-${Math.round(Math.random() * 100000)}`
return Object.assign({key}, data)
})
export const reloadFavorites = createAction('RELOAD_FAVORITES', Favorites.get)
export const removeFavorite = createAction('REMOVE_FAVORITE')
export const reorderFavorites = createAction('REORDER_FAVORITES')
export const updateFavorite = createAction('UPDATE_FAVORITE', (key, data) => ({key, data}))
================================================
FILE: src/renderer/redux/actions/index.js
================================================
export * from './instances'
export * from './favorites'
export * from './patterns'
export * from './connection'
export * from './sizes'
================================================
FILE: src/renderer/redux/actions/instances.js
================================================
import {createAction} from 'Utils';
import {remote} from 'electron'
import {getId} from 'Utils'
export const createInstance = createAction('CREATE_INSTANCE', data => (
Object.assign({}, data, {key: getId('instance')})
))
export const selectInstance = createAction('SELECT_INSTANCE')
export const moveInstance = createAction('MOVE_INSTANCE', (from, to) => ({getState, next}) => {
const {instances} = getState()
const [fromIndex, instance] = instances.findEntry(v => v.get('key') === from);
const toIndex = instances.findIndex(v => v.get('key') === to);
next({fromIndex, toIndex, activeInstanceKey: instance.get('key')})
})
export const delInstance = createAction('DEL_INSTANCE', key => ({getState, next}) => {
const {activeInstanceKey, instances} = getState()
if (!key) {
key = activeInstanceKey
}
const targetIndex = instances.findIndex(instance => instance.get('key') === key);
const ret = {activeInstanceKey, targetIndex}
if (key === activeInstanceKey) {
const item = instances.get(targetIndex + 1) || (targetIndex > 0 && instances.get(targetIndex - 1))
console.log('still', item, targetIndex, instances.size)
if (item) {
ret.activeInstanceKey = item.get('key')
} else {
remote.getCurrentWindow().close();
return;
}
}
next(ret)
})
================================================
FILE: src/renderer/redux/actions/patterns.js
================================================
import {createAction} from 'Utils'
import {Patterns} from '../../storage'
export const createPattern = createAction('CREATE_PATTERN', (conn) => {
const key = `pattern-${Math.round(Math.random() * 100000)}`
return Object.assign({key, conn})
})
export const reloadPatterns = createAction('RELOAD_PATTERNS', Patterns.get)
export const removePattern = createAction('REMOVE_PATTERN', (conn, index) => ({conn, index}))
export const updatePattern = createAction('UPDATE_PATTERN', (conn, index, data) => ({conn, index, data}))
================================================
FILE: src/renderer/redux/actions/sizes.js
================================================
import {createAction} from 'Utils';
export const setSize = createAction('SET_SIZE', (type, value) => ({type, value: Number(value)}))
================================================
FILE: src/renderer/redux/middlewares/createThunkReplyMiddleware.js
================================================
function isThunkReply(action) {
return typeof action.payload === 'function' && action.args
}
export default function createThunkReplyMiddleware(extraArgument) {
return function ({dispatch, getState}) {
return _next => action => {
if (!isThunkReply(action)) {
return _next(action)
}
function next(payload) {
dispatch({payload, type: action.type})
}
return action.payload(Object.assign({dispatch, getState, next}, extraArgument))
}
}
}
================================================
FILE: src/renderer/redux/middlewares/index.js
================================================
import createThunkReplyMiddleware from './createThunkReplyMiddleware'
export {createThunkReplyMiddleware}
================================================
FILE: src/renderer/redux/persistEnhancer.js
================================================
import * as Storage from '../storage'
const whiteList = [
{key: 'patterns', storage: 'Patterns'},
{key: 'favorites', storage: 'Favorites'},
{key: 'sizes', storage: 'Sizes'}
]
export default function (store) {
let lastState
store.subscribe(() => {
if (store.skipPersist) {
return
}
const state = store.getState()
whiteList.forEach(({key, storage}) => {
const value = state[key]
if (!lastState || value !== lastState[key]) {
Storage[storage].set(value)
}
})
lastState = state
})
}
================================================
FILE: src/renderer/redux/reducers/activeInstanceKey.js
================================================
import {handleActions} from 'Utils'
import {
createInstance,
selectInstance,
moveInstance,
delInstance
} from 'Redux/actions'
export const defaultInstanceKey = 'FIRST_INSTANCE'
export const activeInstanceKey = handleActions(defaultInstanceKey, {
[createInstance](state, data) {
return data.key
},
[selectInstance](state, data) {
return data
},
[moveInstance](state, {activeInstanceKey}) {
return activeInstanceKey
},
[delInstance](state, {activeInstanceKey}) {
console.log('==delInstance', activeInstanceKey)
return activeInstanceKey
}
})
================================================
FILE: src/renderer/redux/reducers/favorites.js
================================================
import {handleActions} from 'Utils'
import {
createFavorite,
removeFavorite,
updateFavorite,
reorderFavorites,
reloadFavorites
} from 'Redux/actions'
import {Favorites} from '../../storage'
import {Map, fromJS} from 'immutable'
function FavoriteFactory(data) {
return Map(Object.assign({name: 'New Favorite'}, data))
}
export const favorites = handleActions(fromJS(Favorites.get()), {
[createFavorite](state, data) {
return state.push(FavoriteFactory(data))
},
[removeFavorite](state, key) {
return state.filterNot(item => item.get('key') === key)
},
[updateFavorite](state, {key, data}) {
return state.map(item => item.get('key') === key ? item.merge(data) : item)
},
[reorderFavorites](state, {from, to}) {
const target = state.get(from);
return state.splice(from, 1).splice(to, 0, target);
},
[reloadFavorites](state, data) {
return fromJS(data)
}
})
================================================
FILE: src/renderer/redux/reducers/index.js
================================================
'use strict';
import {combineReducers} from 'redux';
import {activeInstanceKey} from './activeInstanceKey'
import {instances} from './instances'
import {favorites} from './favorites'
import {patterns} from './patterns'
import {sizes} from './sizes'
export default combineReducers({
patterns,
favorites,
instances,
activeInstanceKey,
sizes
});
================================================
FILE: src/renderer/redux/reducers/instances.js
================================================
import {handleActions} from 'Utils'
import {
createInstance,
moveInstance,
delInstance,
updateConnectStatus,
connectToRedis,
disconnect
} from 'Redux/actions'
import {Map, List} from 'immutable'
import {defaultInstanceKey} from './activeInstanceKey'
function InstanceFactory({key, data}) {
return Map(Object.assign({key, title: 'Medis'}, data))
}
export const instances = handleActions(List([InstanceFactory({key: defaultInstanceKey})]), {
[createInstance](state, data) {
return state.push(InstanceFactory({data}))
},
[moveInstance](state, {fromIndex, toIndex}) {
const instance = state.get(fromIndex)
return state.splice(fromIndex, 1).splice(toIndex, 0, instance)
},
[delInstance](state, {targetIndex}) {
return state.remove(targetIndex)
},
[updateConnectStatus](state, {index, status}) {
return state.setIn([index, 'connectStatus'], status)
},
[disconnect](state, {index}) {
const properties = ['connectStatus', 'redis', 'config', 'version']
return state.update(index, instance => (
instance.withMutations(map => {
properties.forEach(key => map.remove(key))
map.set('title', 'Medis')
})
))
},
[connectToRedis](state, {index, config, redis}) {
const {name, sshHost, host, port} = config
const remote = name ? `${name}/` : (sshHost ? `${sshHost}/` : '')
const address = `${host}:${port}`
const title = `${remote}${address}`
const connectionKey = `${sshHost || ''}|${host}|${port}`
const version = redis.serverInfo && redis.serverInfo.redis_version
return state.update(index, instance => (
instance.merge({config, title, redis, version, connectionKey}).remove('connectStatus')
))
}
})
================================================
FILE: src/renderer/redux/reducers/patterns.js
================================================
import {handleActions} from 'Utils'
import {
createPattern,
removePattern,
updatePattern,
reloadPatterns
} from 'Redux/actions'
import {Patterns} from '../../storage'
import {Map, List, fromJS} from 'immutable'
function PatternFactory(data) {
return Map(Object.assign({value: '*', name: '*'}, data))
}
export const patterns = handleActions(fromJS(Patterns.get()), {
[createPattern](state, {conn, key}) {
return state.update(conn, List(), patterns => patterns.push(PatternFactory({key})))
},
[removePattern](state, {conn, index}) {
return state.update(conn, List(), patterns => patterns.remove(index))
},
[updatePattern](state, {conn, index, data}) {
return state.update(conn, List(), patterns => patterns.update(index, item => item.merge(data)))
},
// [reorderPatterns](state, {conn, from, to}) {
// return state.update(conn, List(), patterns => {
// const target = patterns.get(from);
// return patterns.splice(from, 1).splice(to, 0, target);
// })
// },
[reloadPatterns](state, data) {
return fromJS(data)
}
})
================================================
FILE: src/renderer/redux/reducers/sizes.js
================================================
import {handleActions} from 'Utils'
import {
setSize
} from 'Redux/actions'
import {Sizes} from '../../storage'
import {Map, List, fromJS} from 'immutable'
export const sizes = handleActions(fromJS(Sizes.get()), {
[setSize](state, {type, value}) {
return state.set(`${type}BarWidth`, value)
}
})
================================================
FILE: src/renderer/redux/store.js
================================================
import {compose, createStore, applyMiddleware} from 'redux'
import persistEnhancer from './persistEnhancer'
import {createThunkReplyMiddleware} from 'Redux/middlewares'
import reducers from './reducers'
const store = window.store = createStore(
reducers,
applyMiddleware(createThunkReplyMiddleware())
)
persistEnhancer(store)
export default store
================================================
FILE: src/renderer/storage/Favorites.js
================================================
'use strict'
import {ipcRenderer} from 'electron'
export function get() {
const data = localStorage.getItem('favorites')
return data ? JSON.parse(data) : []
}
export function set(favorites) {
localStorage.setItem('favorites', JSON.stringify(favorites))
ipcRenderer.send('dispatch', 'reloadFavorites')
return favorites
}
================================================
FILE: src/renderer/storage/Patterns.js
================================================
'use strict'
import {ipcRenderer} from 'electron'
export function get() {
const data = localStorage.getItem('patternStore')
return data ? JSON.parse(data) : {}
}
export function set(patterns) {
localStorage.setItem('patternStore', JSON.stringify(patterns))
ipcRenderer.send('dispatch', 'reloadPatterns')
return patterns
}
================================================
FILE: src/renderer/storage/Sizes.js
================================================
'use strict'
export function get() {
const data = localStorage.getItem('sizes')
return data ? JSON.parse(data) : {}
}
export function set(sizes) {
localStorage.setItem('sizes', JSON.stringify(sizes))
return sizes
}
================================================
FILE: src/renderer/storage/index.js
================================================
import * as Favorites from './Favorites'
import * as Patterns from './Patterns'
import * as Sizes from './Sizes'
export {Favorites, Patterns, Sizes}
================================================
FILE: src/renderer/styles/global.scss
================================================
@import "photon";
@import "native";
html {
background: #ececec;
}
ul {
margin: 0;
padding: 0;
}
.sidebar {
background: #f5f5f4;
display: flex;
flex-direction: column;
.nav-group {
overflow: auto;
flex: 1;
}
}
.main {
position: relative;
flex: 1;
}
.nav-group {
.sortable-placeholder {
width: 100%;
height: 26px;
}
}
textarea {
&:focus {
outline: none;
}
}
.Pane.vertical {
height: 100%;
display: flex;
min-width: 0;
}
.fixedDataTableCellLayout_columnResizerContainer {
border-right: 1px solid #ccc;
}
================================================
FILE: src/renderer/styles/native.scss
================================================
.nt-box {
box-sizing: border-box;
position: relative;
cursor: default;
background-color: rgba(0, 0, 0, .04);
border-width: 1px;
border-style: solid;
border-top-color: rgba(0, 0, 0, .07);
border-left-color: rgba(0, 0, 0, .037);
border-right-color: rgba(0, 0, 0, .037);
border-bottom-color: rgba(0, 0, 0, .026);
border-radius: 4px;
padding: 23px 18px 22px 18px;
}
.nt-form-row, .form-control {
padding: 5px 0;
$label-width: 140px;
label {
float: left;
width: $label-width;
text-align: right;
-webkit-user-select: text;
}
input, textarea, select {
margin-left: 10px;
}
input[type="text"], input[type="number"], input[type="password"], select {
width: 250px;
-webkit-user-select: text;
border-width: 1px;
border-style: solid;
border-color: #b0b0b0;
border-left-color: #b1b1b1;
border-right-color: #b1b1b1;
box-shadow: inset 0 0 0 1px #f0f0f0;
padding-top: 4px;
padding-bottom: 4px;
padding-left: 3.5px;
padding-right: 3.5px;
line-height: 14px;
font-family: "San Francisco", "Helvetica Neue", "Lucida Grande", Arial, sans-serif;
font-size: 13px;
background: #fff;
&:focus {
outline: none;
border-color: #6691d6;
box-shadow: 0 0 0 2.5px #7ba7ec;
border-radius: 1px;
}
&:placeholder {
color: #c0c0c0;
}
&[disabled] {
background: #f8f8f8;
}
}
input[type="radio"],
input[type="checkbox"] {
line-height: normal;
}
&.nt-form-row--vertical {
overflow: visible;
label {
float: none;
display: block;
text-align: left;
}
input[type="text"], input[type="password"], textarea {
margin-left: 0;
width: 100%;
}
}
}
.nt-button {
cursor: default;
background-color: #ffffff;
outline: none;
border-width: 1px;
border-style: solid;
border-radius: 3px;
border-top-color: #c8c8c8;
border-bottom-color: #acacac;
border-left-color: #c2c2c2;
border-right-color: #c2c2c2;
box-shadow: 0 1px rgba(0, 0, 0, .039);
padding-top: 0;
padding-bottom: 0;
padding-left: 13px;
padding-right: 13px;
line-height: 19px;
font-size: 13px;
&:active {
background-image: -webkit-linear-gradient(top, #4c98fe 0%, #0564e3 100%);
border-top-color: #247fff;
border-bottom-color: #003ddb;
border-left-color: #125eed;
border-right-color: #125eed;
color: rgba(255, 255, 255, .9);
}
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
.nt-button--primary {
background-image: -webkit-linear-gradient(top, #6ca5fc 0%, #076aff 100%);
border-top-color: #4c93fa;
border-bottom-color: #0261ff;
border-left-color: #2d7efc;
border-right-color: #2d7efc;
color: rgba(255, 255, 255, .9);
&:active {
background-image: -webkit-linear-gradient(top, #4c98fe 0%, #0564e3 100%);
border-top-color: #247fff;
border-bottom-color: #003ddb;
border-left-color: #125eed;
border-right-color: #125eed;
color: rgba(255, 255, 255, .9);
}
}
.nt-button:focus {
outline: none;
border-color: #6691d6;
box-shadow: 0 0 0 2.5px #7ba7ec;
}
.nt-button[disabled], .nt-button--disabled {
background: #f1f1f1;
border-color: #d0d0d0;
color: #b1b1b1;
}
.nt-button-group {
text-align: center;
.nt-button {
display: inline-block;
}
&.nt-button-group--pull-right {
text-align: right;
}
}
================================================
FILE: src/renderer/styles/photon.scss
================================================
.tab-item-btn {
width: 30px;
flex: none;
}
.tab-group {
background: #b3b1b3;
}
.nav-group-item:active {
background-color: transparent !important;
}
.nav-group-item.sortable-ghost {
opacity: 0;
}
.nav-group-item.active:active {
background-color: #dcdfe1 !important;
}
.window {
background: #ececec !important;
}
.toolbar-footer {
button {
width: 30px;
border: none;
border-right: 1px solid #c2c0c2;
box-shadow: 1px 0px 0px 0px rgba(255, 255, 255, 0.4);
background: transparent;
font-size: 18px;
line-height: 19px;
opacity: 0.8;
&:active {
background-color: #dcdfe1;
}
}
}
button:focus {
outline:0;
}
================================================
FILE: src/renderer/utils.ts
================================================
import {createAction as _createAction} from 'redux-actions'
export function handleActions(defaultState, handlers) {
return function (state = defaultState, {type, payload}) {
const handler = handlers[type]
return handler ? handler(state, payload) : state
}
}
export const getId = (function () {
const ids = {}
return function (item: string) {
if (!ids[item]) {
ids[item] = 0
}
return `${item}-${++ids[item] + (Math.random() * 100000 | 0)}`
}
}())
export function createAction(type: string, payloadCreator, metaCreator) {
type = `$SOS_${type}`
const actionCreator = _createAction(type, payloadCreator, metaCreator)
const creator = (...args) => {
const action = actionCreator(...args)
if (typeof action.payload === 'function') {
return Object.assign(action, {args})
}
return action
}
return Object.assign(creator, {
toString: actionCreator.toString,
payload(payload) {
return {type, payload}
},
reply(args, result) {
return {type, payload: {args, result}}
}
})
}
================================================
FILE: src/renderer/vendors/jquery.terminal/index.css
================================================
/*
* This css file is part of jquery terminal
*
* Licensed under GNU LGPL Version 3 license
* Copyright (c) 2011-2013 Jakub Jankiewicz
*
*/
.terminal .terminal-output .format, .cmd .format,
.cmd .prompt, .cmd .prompt div, .terminal .terminal-output div div{
display: inline-block;
}
.cmd .clipboard {
position: absolute;
bottom: 0;
left: 0;
opacity: 0.01;
filter: alpha(opacity = 0.01);
filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0.01);
width: 2px;
}
.cmd > .clipboard {
position: fixed;
}
.terminal {
padding: 10px;
position: relative;
overflow: hidden;
}
.cmd {
padding: 0;
margin: 0;
height: 1.3em;
/*margin-top: 3px; */
}
.cmd .cursor.blink {
-webkit-animation: blink 1s infinite steps(1, start);
-moz-animation: blink 1s infinite steps(1, start);
-ms-animation: blink 1s infinite steps(1, start);
animation: blink 1s infinite steps(1, start);
}
@keyframes blink {
0%, 100% {
background-color: #000;
color: #aaa;
}
50% {
background-color: #bbb; /* not #aaa because it's seem there is Google Chrome bug */
color: #000;
}
}
@-webkit-keyframes blink {
0%, 100% {
background-color: #000;
color: #aaa;
}
50% {
background-color: #bbb;
color: #000;
}
}
@-ms-keyframes blink {
0%, 100% {
background-color: #000;
color: #aaa;
}
50% {
background-color: #bbb;
color: #000;
}
}
@-moz-keyframes blink {
0%, 100% {
background-color: #000;
color: #aaa;
}
50% {
background-color: #bbb;
color: #000;
}
}
.terminal .terminal-output div div, .cmd .prompt {
display: block;
line-height: 18px;
height: auto;
}
.cmd .prompt {
float: left;
}
.terminal, .cmd {
font-family: monospace;
color: #eed1b3;
background-color: #202020;
font-size: 14px;
line-height: 18px;
}
.terminal-output > div {
/*padding-top: 3px;*/
min-height: 14px;
}
.terminal .terminal-output div span {
display: inline-block;
}
.cmd span {
float: left;
/*display: inline-block; */
}
.terminal .inverted, .cmd .inverted, .cmd .cursor.blink {
background-color: #aaa;
color: #000;
}
.terminal .terminal-output div div::-moz-selection,
.terminal .terminal-output div span::-moz-selection,
.terminal .terminal-output div div a::-moz-selection {
background-color: #aaa;
color: #000;
}
.terminal .terminal-output div div::selection,
.terminal .terminal-output div div a::selection,
.terminal .terminal-output div span::selection,
.cmd > span::selection,
.cmd .prompt span::selection {
background-color: #aaa;
color: #000;
}
.terminal .terminal-output div.error, .terminal .terminal-output div.error div {
color: red;
}
.tilda {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 1100;
}
.clear {
clear: both;
}
.terminal a {
color: #0F60FF;
}
.terminal a:hover {
color: red;
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Config/index.jsx
================================================
'use strict'
import React from 'react'
import Immutable from 'immutable'
import {remote} from 'electron'
import fs from 'fs'
require('./index.scss')
class Config extends React.PureComponent {
state = {
data: new Immutable.Map()
}
getProp(property) {
if (this.state.data.has(property)) {
return this.state.data.get(property)
}
return (this.props.favorite ? this.props.favorite.get(property) : '') || ''
}
setProp(property, value) {
this.setState({
data: typeof property === 'string' ? this.state.data.set(property, value) : this.state.data.merge(property),
changed: Boolean(this.props.favorite)
})
}
componentWillReceiveProps(nextProps) {
if (!this.props.connect && nextProps.connect) {
this.connect()
}
if (this.props.favorite || nextProps.favorite) {
const leaving = !this.props.favorite || !nextProps.favorite ||
(this.props.favorite.get('key') !== nextProps.favorite.get('key'))
if (leaving) {
this.setState({changed: false, data: new Immutable.Map()})
}
}
}
connect() {
const {favorite, connectToRedis} = this.props
const data = this.state.data
const config = favorite ? favorite.merge(data).toJS() : data.toJS()
config.host = config.host || 'localhost'
config.port = config.port || '6379'
config.sshPort = config.sshPort || '22'
connectToRedis(config)
this.save()
}
handleChange(property, e) {
let value = e.target.value
if (property === 'ssh' || property === 'ssl') {
value = e.target.checked
}
this.setProp(property, value)
}
duplicate() {
if (this.props.favorite) {
const data = Object.assign(this.props.favorite.toJS(), this.state.data.toJS())
delete data.key
this.props.onDuplicate(data)
} else {
const data = this.state.data.toJS()
data.name = 'Quick Connect'
this.props.onDuplicate(data)
}
}
save() {
if (this.props.favorite && this.state.changed) {
this.props.onSave(this.state.data.toJS())
this.setState({changed: false, data: new Immutable.Map()})
}
}
renderCertInput(label, id) {
return (
{label}:
{
const win = remote.getCurrentWindow()
const files = remote.dialog.showOpenDialog(win, {
properties: ['openFile']
})
if (files && files.length) {
const file = files[0]
const content = fs.readFileSync(file, 'utf8')
this.setProp({[id]: content, [`${id}File`]: file})
}
}}
/>
)
}
render() {
return (
{
this.duplicate()
}}
>{this.props.favorite ? 'Duplicate' : 'Add to Favorite'}
{
this.save()
}}
>Save Changes
{
this.connect()
}}
>{this.props.connectStatus || (this.state.changed ? 'Save and Connect' : 'Connect')}
)
}
}
export default Config
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Config/index.scss
================================================
#sshPassword {
padding-right: 32px;
}
.ssh-key {
height: 22px;
line-height: 22px;
padding: 0;
text-align: center;
width: 30px;
position: relative;
top: 0;
left: -30px;
border-top: 0;
border-bottom: 0;
border-right: 1px solid #b1b1b1;
border-left: 1px solid #b1b1b1;
background: linear-gradient(to bottom, #fafafa 0%, #f5f5f5 100%);
&.is-active {
color: #388af8;
}
&:active {
background: linear-gradient(to bottom, #c2c2c2 0%, #b6b5b6 100%);
color: #388af8;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Favorite.jsx
================================================
'use strict'
import React from 'react'
import Sortable from 'sortablejs'
class Favorite extends React.PureComponent {
constructor() {
super()
this.state = {
activeKey: null
}
this._updateSortableKey()
}
_updateSortableKey() {
this.sortableKey = `sortable-${Math.round(Math.random() * 10000)}`
}
_bindSortable() {
const {reorderFavorites} = this.props
this.sortable = Sortable.create(this.refs.sortable, {
animation: 100,
onStart: evt => {
this.nextSibling = evt.item.nextElementSibling
},
onAdd: () => {
this._updateSortableKey()
},
onUpdate: evt => {
this._updateSortableKey()
reorderFavorites({from: evt.oldIndex, to: evt.newIndex})
}
})
}
componentDidMount() {
this._bindSortable()
}
componentDidUpdate() {
this._bindSortable()
}
onClick(index, evt) {
evt.preventDefault()
this.selectIndex(index)
}
onDoubleClick(index, evt) {
evt.preventDefault()
this.selectIndex(index, true)
}
selectIndex(index, connect) {
this.select(index === -1 ? null : this.props.favorites.get(index), connect)
}
select(favorite, connect) {
const activeKey = favorite ? favorite.get('key') : null
this.setState({activeKey})
if (connect) {
this.props.onRequireConnecting(activeKey)
} else {
this.props.onSelect(activeKey)
}
}
render() {
return (
QUICK CONNECT
FAVORITES
{
this.props.createFavorite()
// TODO: auto select
// this.select(favorite);
}}
>+
{
const key = this.state.activeKey
if (!key) {
return
}
showModal({
title: 'Delete the bookmark?',
button: 'Delete',
content: 'Are you sure you want to delete the selected bookmark? This action cannot be undone.'
}).then(() => {
const index = this.props.favorites.findIndex(favorite => key === favorite.get('key'))
this.props.removeFavorite(key)
this.selectIndex(index - 1)
})
}
}
>-
)
}
componentWillUnmount() {
this.sortable.destroy()
}
}
export default Favorite
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/index.jsx
================================================
'use strict'
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import Favorite from './Favorite'
import Config from './Config'
import {connectToRedis} from 'Redux/actions'
import {removeFavorite, updateFavorite, createFavorite, reorderFavorites} from 'Redux/actions'
class ConnectionSelector extends PureComponent {
state = {connect: false, key: null}
handleSelectFavorite(connect, key) {
this.setState({connect, key})
}
render() {
const selectedFavorite = this.state.key && this.props.favorites.find(item => item.get('key') === this.state.key)
return (
{
this.props.updateFavorite(selectedFavorite.get('key'), data)
}}
onDuplicate={this.props.createFavorite}
/>
)
}
}
function mapStateToProps(state, {instance}) {
return {
favorites: state.favorites,
connectStatus: instance.get('connectStatus')
}
}
const mapDispatchToProps = {
updateFavorite,
createFavorite,
connectToRedis,
reorderFavorites,
removeFavorite
}
export default connect(mapStateToProps, mapDispatchToProps)(ConnectionSelector)
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/AddButton/index.jsx
================================================
import React, {memo} from 'react'
require('./index.scss')
function AddButton({title, reload, onReload, onClick}) {
return (
{title}
{reload && }
+
)
}
export default memo(AddButton)
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/AddButton/index.scss
================================================
.AddButton {
position: relative;
span.plus, span.reload {
position: absolute;
right: 4px;
top: 4px;
width: 16px;
height: 16px;
line-height: 13px;
border: 1px solid #ccc;
border-radius: 2px;
background-image: linear-gradient(#fff, #efefef);
text-align: center;
font-weight: normal;
color: #888;
&:hover {
background: #fff;
}
&:active {
background: #efefef;
}
}
span.reload {
right: 24px;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Config/index.jsx
================================================
'use strict'
import React from 'react'
import clone from 'lodash.clone'
require('./index.scss')
class Config extends React.Component {
constructor(props) {
super(props)
this.writeableFields = [
'dbfilename',
'requirepass',
'masterauth',
'maxclients',
'appendonly',
'save',
'dir',
'client-output-buffer-limit',
'notify-keyspace-events',
'rdbcompression',
'repl-disable-tcp-nodelay',
'repl-diskless-sync',
'cluster-require-full-coverage',
'aof-rewrite-incremental-fsync',
'aof-load-truncated',
'slave-serve-stale-data',
'slave-read-only',
'activerehashing',
'stop-writes-on-bgsave-error',
'lazyfree-lazy-eviction',
'lazyfree-lazy-expire',
'lazyfree-lazy-server-del',
'slave-lazy-flush',
'tcp-keepalive',
'maxmemory-samples',
'timeout',
'auto-aof-rewrite-percentage',
'auto-aof-rewrite-min-size',
'hash-max-ziplist-entries',
'hash-max-ziplist-value',
'list-max-ziplist-entries',
'list-max-ziplist-value',
'list-max-ziplist-size',
'list-compress-depth',
'set-max-intset-entries',
'zset-max-ziplist-entries',
'zset-max-ziplist-value',
'hll-sparse-max-bytes',
'lua-time-limit',
'slowlog-log-slower-than',
'slowlog-max-len',
'latency-monitor-threshold',
'repl-ping-slave-period',
'repl-timeout',
'repl-backlog-ttl',
'repl-diskless-sync-delay',
'slave-priority',
'min-slaves-to-write',
'min-slaves-max-lag',
'cluster-node-timeout',
'cluster-migration-barrier',
'cluster-slave-validity-factor',
'hz',
'watchdog-period',
'maxmemory',
'repl-backlog-size',
'loglevel',
'maxmemory-policy',
'appendfsync'
]
this.groups = [
{
name: 'General',
configs: [
{name: 'port', type: 'number'},
{name: 'bind'},
{name: 'unixsocket'},
{name: 'unixsocketperm', type: 'number'},
{name: 'daemonize', type: 'boolean'},
{name: 'pidfile'},
{name: 'tcp-backlog', type: 'number'},
{name: 'tcp-keepalive', type: 'number'},
{name: 'timeout', type: 'number'},
{name: 'databases', type: 'number'}
]
},
{
name: 'Logging',
configs: [
{name: 'loglevel', type: ['debug', 'verbose', 'notice', 'warning']},
{name: 'logfile'},
{name: 'syslog-enabled', type: 'boolean'},
{name: 'syslog-ident'},
{name: 'syslog-facility'}
]
},
{
name: 'Snap Shotting',
configs: [
{name: 'dbfilename'},
{name: 'dir'},
{name: 'save'},
{name: 'stop-writes-on-bgsave-error', type: 'boolean'},
{name: 'rdbcompression', type: 'boolean'},
{name: 'rdbchecksum', type: 'boolean'}
]
},
{
name: 'Replication',
configs: [
{name: 'slaveof'},
{name: 'masterauth'},
{name: 'slave-serve-stale-data', type: 'boolean'},
{name: 'slave-read-only', type: 'boolean'},
{name: 'repl-diskless-sync', type: 'boolean'},
{name: 'repl-diskless-sync-delay', type: 'number'},
{name: 'repl-ping-slave-period', type: 'number'},
{name: 'repl-timeout', type: 'number'},
{name: 'repl-disable-tcp-nodelay', type: 'boolean'},
{name: 'repl-backlog-size'},
{name: 'repl-backlog-ttl', type: 'number'},
{name: 'slave-priority', type: 'number'},
{name: 'min-slaves-to-write', type: 'number'},
{name: 'min-slaves-max-lag', type: 'number'}
]
},
{
name: 'Security',
configs: [
{name: 'requirepass'},
{name: 'rename-command'}
]
},
{
name: 'Limits',
configs: [
{name: 'maxclients'},
{name: 'maxmemory'},
{name: 'maxmemory-policy', type: ['noeviction', 'volatile-lru', 'allkeys-lru', 'volatile-random', 'allkeys-random', 'volatile-ttl']},
{name: 'maxmemory-samples', type: 'number'}
]
},
{
name: 'Append Only Mode',
configs: [
{name: 'appendonly', type: 'boolean'},
{name: 'appendfilename'},
{name: 'appendfsync', type: ['everysec', 'always', 'no']},
{name: 'no-appendfsync-on-rewrite', type: 'boolean'},
{name: 'auto-aof-rewrite-percentage', type: 'number'},
{name: 'auto-aof-rewrite-min-size'},
{name: 'aof-load-truncated', type: 'number'}
]
},
{
name: 'LUA Scripting',
configs: [
{name: 'lua-time-limit', type: 'number'}
]
},
{
name: 'Cluster',
configs: [
{name: 'cluster-enabled', type: 'boolean'},
{name: 'cluster-config-file'},
{name: 'cluster-node-timeout', type: 'number'},
{name: 'cluster-slave-validity-factor', type: 'nubmer'},
{name: 'cluster-migration-barrier', type: 'number'},
{name: 'cluster-require-full-coverage', type: 'boolean'}
]
},
{
name: 'Slow Log',
configs: [
{name: 'slowlog-log-slower-than', type: 'number'},
{name: 'slowlog-max-len', type: 'number'}
]
},
{
name: 'Latency Monitor',
configs: [
{name: 'latency-monitor-threshold', type: 'number'}
]
},
{
name: 'Event Notification',
configs: [
{name: 'notify-keyspace-events'}
]
},
{
name: 'Advanced Config',
configs: [
{name: 'hash-max-ziplist-entries', type: 'number'},
{name: 'hash-max-ziplist-value', type: 'number'},
{name: 'list-max-ziplist-entries', type: 'number'},
{name: 'list-max-ziplist-value', type: 'number'},
{name: 'set-max-intset-entries', type: 'number'},
{name: 'zset-max-ziplist-entries', type: 'number'},
{name: 'zset-max-ziplist-value', type: 'number'},
{name: 'hll-sparse-max-bytes', type: 'number'},
{name: 'activerehashing', type: 'boolean'},
{name: 'client-output-buffer-limit'},
{name: 'hz', type: 'number'},
{name: 'aof-rewrite-incremental-fsync', type: 'boolean'}
]
}
]
this.state = {
groups: [],
unsavedRewrites: {},
unsavedConfigs: {}
}
this.load()
}
load() {
this.props.redis.config('get', '*').then(config => {
const configs = {}
for (let i = 0; i < config.length - 1; i += 2) {
configs[config[i]] = config[i + 1]
}
const groups = clone(this.groups, true).map(g => {
g.configs = g.configs.map(c => {
if (typeof configs[c.name] !== 'undefined') {
c.value = configs[c.name]
delete configs[c.name]
}
return c
}).filter(c => typeof c.value !== 'undefined')
return g
}).filter(g => g.configs.length)
if (Object.keys(configs).length) {
groups.push({name: 'Other', configs: Object.keys(configs).map(key => {
return {
name: key,
value: configs[key]
}
})})
}
this.setState({
groups,
unsavedConfigs: {},
unsavedRewrites: {}
})
})
}
componentWillUnmount() {
this.props.redis.removeAllListeners('select', this.onSelectBinded)
}
renderGroup(group) {
return (
{group.name}
{ group.configs.map(this.renderConfig, this) }
)
}
change({name, value}) {
this.state.unsavedConfigs[name] = value
this.state.unsavedRewrites[name] = value
this.setState({
groups: this.state.groups,
unsavedConfigs: this.state.unsavedConfigs,
unsavedRewrites: this.state.unsavedRewrites
})
}
renderConfig(config) {
let input
const props = {readOnly: this.writeableFields.indexOf(config.name) === -1}
props.disabled = props.readOnly
if (config.type === 'boolean' &&
(config.value === 'yes' || config.value === 'no')) {
input = ( {
config.value = e.target.checked ? 'yes' : 'no'
this.change(config)
}} {...props}
/>)
} else if (config.type === 'number' && String(parseInt(config.value, 10)) === config.value) {
input = ( {
config.value = e.target.value
this.change(config)
}} {...props}
/>)
} else if (Array.isArray(config.type) && config.type.indexOf(config.value) !== -1) {
input = ( {
config.value = e.target.value
this.change(config)
}} {...props}
>
{config.type.map(option => {option} )}
)
} else {
input = ( {
config.value = e.target.value
this.change(config)
}} {...props}
/>)
}
return (
{config.name}
{ input }
{config.description}
)
}
isChanged(rewrite) {
return Object.keys(this.state[rewrite ? 'unsavedRewrites' : 'unsavedConfigs']).length > 0
}
handleReload() {
if (this.isChanged()) {
showModal({
title: 'Reload config?',
content: 'Are you sure you want to reload the config? Your changes will be lost if you reload the config.',
button: 'Reload'
}).then(() => {
this.load()
})
} else {
this.load()
}
}
handleSave() {
showModal({
title: 'Save the changes',
content: 'Are you sure you want to apply the changes and save the changes to the config file?',
button: 'Save'
}).then(() => {
return this.handleApply(true)
}).then(res => {
return this.props.redis.config('rewrite')
}).then(res => {
this.setState({unsavedRewrites: {}})
}).catch(err => {
alert(err.message)
})
}
handleApply(embed) {
const confirm = embed ? Promise.resolve(1) : showModal({
title: 'Apply the changes',
content: 'Are you sure you want to apply the changes? The changes are only valid for the current session and will be lost when Redis restarts.',
button: 'Apply'
})
return confirm.then(() => {
const pipeline = this.props.redis.pipeline()
const unsavedConfigs = this.state.unsavedConfigs
Object.keys(unsavedConfigs).forEach(config => {
pipeline.config('set', config, unsavedConfigs[config])
})
return pipeline.exec()
}).then(res => {
this.setState({unsavedConfigs: {}})
})
}
render() {
return (
Reload
Save To Config File
{
this.handleApply()
}}
>Apply
)
}
}
export default Config
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Config/index.scss
================================================
.Config {
overflow: auto;
.wrapper {
width: 430px;
margin: 0 auto;
.nt-form-row label {
width: 170px;
}
}
h3 {
font-size: 20px;
}
.nt-button-group {
margin: 20px 0;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Footer.jsx
================================================
'use strict'
import React from 'react'
import humanFormat from 'human-format'
const timeScale = new humanFormat.Scale({
ms: 1,
s: 1000,
min: 60000,
h: 3600000,
d: 86400000
})
const initState = {
ttl: null,
encoding: null,
size: null
}
class Footer extends React.PureComponent {
state = initState
init(keyName, keyType) {
if (!keyType && keyType !== 'none') {
this.setState(initState)
return
}
const pipeline = this.props.redis.pipeline()
pipeline.pttl(keyName)
pipeline.object('ENCODING', keyName)
let sizeUnit = 'Members'
switch (keyType) {
case 'string': pipeline.strlen(keyName); sizeUnit = 'Bytes'; break
case 'hash': pipeline.hlen(keyName); break
case 'list': pipeline.llen(keyName); break
case 'set': pipeline.scard(keyName); break
case 'zset': pipeline.zcard(keyName); break
}
pipeline.exec((err, [[err1, pttl], [err2, encoding], res3]) => {
this.setState({
encoding: encoding ? `Encoding: ${encoding}` : '',
ttl: pttl >= 0 ? `TTL: ${humanFormat(pttl, {scale: timeScale}).replace(' ', '')}` : null,
size: (res3 && res3[1]) ? `${sizeUnit}: ${res3[1]}` : null
})
})
}
componentDidMount() {
this.init(this.props.keyName, this.props.keyType)
}
componentWillReceiveProps(nextProps) {
if (nextProps.keyName !== this.props.keyName ||
nextProps.keyType !== this.props.keyType ||
nextProps.version !== this.props.version) {
this.init(nextProps.keyName, nextProps.keyType)
}
}
render() {
const desc = ['size', 'encoding', 'ttl']
.map(key => ({key, value: this.state[key]}))
.filter(item => typeof item.value === 'string')
return (
{
desc.map(({key, value}) => {value} )
}
)
}
}
export default Footer
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/Editor/index.jsx
================================================
'use strict'
import React from 'react'
import ReactDOM from 'react-dom'
import Codemirror from 'medis-react-codemirror'
require('codemirror/mode/javascript/javascript')
require('codemirror/addon/lint/json-lint')
require('codemirror/addon/lint/lint')
require('codemirror/addon/selection/active-line')
require('codemirror/addon/edit/closebrackets')
require('codemirror/addon/edit/matchbrackets')
require('codemirror/addon/search/search')
require('codemirror/addon/search/searchcursor')
require('codemirror/addon/search/jump-to-line')
require('codemirror/addon/dialog/dialog')
require('codemirror/addon/dialog/dialog.css')
import jsonlint from 'jsonlint'
window.jsonlint = jsonlint.parser
require('codemirror/lib/codemirror.css')
require('codemirror/addon/lint/lint.css')
const msgpack = require('msgpack5')()
require('./index.scss')
class Editor extends React.PureComponent {
constructor() {
super()
this.resizeObserver = new ResizeObserver(() => {
this.updateLayout()
})
this.state = {
currentMode: '',
wrapping: true,
changed: false,
modes: {
raw: false,
json: false,
messagepack: false
}
}
}
updateLayout() {
const {wrapSelector, codemirror} = this.refs
const $this = $(ReactDOM.findDOMNode(this))
if ($this.width() < 372) {
$(ReactDOM.findDOMNode(wrapSelector)).hide()
} else {
$(ReactDOM.findDOMNode(wrapSelector)).show()
}
if (codemirror) {
codemirror.getCodeMirror().refresh()
}
}
componentDidMount() {
this.init(this.props.buffer)
this.resizeObserver.observe(ReactDOM.findDOMNode(this))
}
componentWillUnmount() {
this.resizeObserver.disconnect()
}
componentWillReceiveProps(nextProps) {
if (nextProps.buffer !== this.props.buffer) {
this.init(nextProps.buffer)
}
}
init(buffer) {
if (!buffer) {
this.setState({currentMode: '', changed: false})
return
}
const content = buffer.toString()
const modes = {}
modes.raw = content
modes.json = tryFormatJSON(content, true)
modes.messagepack = modes.json ? false : tryFormatMessagepack(buffer, true)
let currentMode = 'raw'
if (modes.messagepack) {
currentMode = 'messagepack'
} else if (modes.json) {
currentMode = 'json'
}
this.setState({modes, currentMode, changed: false}, () => {
this.updateLayout()
})
}
save() {
let content = this.state.modes.raw
if (this.state.currentMode === 'json') {
content = tryFormatJSON(this.state.modes.json)
if (!content) {
alert('The json is invalid. Please check again.')
return
}
} else if (this.state.currentMode === 'messagepack') {
content = tryFormatMessagepack(this.state.modes.messagepack)
if (!content) {
alert('The json is invalid. Please check again.')
return
}
content = msgpack.encode(JSON.parse(content))
}
this.props.onSave(content, err => {
if (err) {
alert(`Redis save failed: ${err.message}`)
} else {
this.init(typeof content === 'string' ? Buffer.from(content) : content)
}
})
}
updateContent(mode, content) {
if (this.state.modes[mode] !== content) {
this.state.modes[mode] = content
this.setState({modes: this.state.modes, changed: true})
}
}
updateMode(evt) {
const newMode = evt.target.value
this.setState({currentMode: newMode})
}
focus() {
const codemirror = this.refs.codemirror
if (codemirror) {
const node = ReactDOM.findDOMNode(codemirror)
if (node) {
node.focus()
}
}
}
handleKeyDown(evt) {
if (!evt.ctrlKey && evt.metaKey && evt.keyCode === 83) {
this.save()
evt.preventDefault()
evt.stopPropagation()
}
}
render() {
let viewer
if (this.state.currentMode === 'raw') {
viewer = ( )
} else if (this.state.currentMode === 'json') {
viewer = ( )
} else if (this.state.currentMode === 'messagepack') {
viewer = ( )
} else {
viewer =
}
return ()
}
}
export default Editor
function tryFormatJSON(jsonString, beautify) {
try {
const o = JSON.parse(jsonString)
if (o && typeof o === 'object' && o !== null) {
if (beautify) {
return JSON.stringify(o, null, '\t')
}
return JSON.stringify(o)
}
} catch (e) { /**/ }
return false
}
function tryFormatMessagepack(buffer, beautify) {
try {
let o
if (typeof buffer === 'string') {
o = JSON.parse(buffer)
} else {
o = msgpack.decode(buffer)
}
if (o && typeof o === 'object' && o !== null) {
if (beautify) {
return JSON.stringify(o, null, '\t')
}
return JSON.stringify(o)
}
} catch (e) { /**/ }
return false
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/Editor/index.scss
================================================
.Editor {
position: relative;
min-width: 0;
textarea {
border: none;
width: 100%;
height: 100%;
}
.CodeMirror {
height: auto;
flex: 1;
display: flex;
flex-direction: column;
font-family: Consolas, monospace;
}
.ReactCodeMirror {
position: relative;
flex: 1;
display: flex;
width: 100%;
overflow-y: auto;
&:before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 45px;
z-index: 1;
background: #f7f7f7;
border-right: 1px solid #ddd;
height: 100%;
}
}
.operation-pannel {
z-index: 99;
}
.mode-selector {
position: absolute;
bottom: 10px;
right: 10px;
}
.wrap-selector {
position: absolute;
bottom: 5px;
right: 120px;
padding: 0 10px;
span {
margin-left: 4px;
}
}
button {
position: absolute;
bottom: 10px;
left: 54px;
z-index: 99;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/HashContent.jsx
================================================
'use strict'
import React from 'react'
import BaseContent from '.'
import SplitPane from 'react-split-pane'
import {Table, Column} from 'fixed-data-table-contextmenu'
import Editor from './Editor'
import AddButton from '../../../AddButton'
import ContentEditable from '../../../ContentEditable'
import ReactDOM from 'react-dom'
import {clipboard, remote} from 'electron'
class HashContent extends BaseContent {
save(value, callback) {
if (typeof this.state.selectedIndex === 'number') {
const [key] = this.state.members[this.state.selectedIndex]
this.state.members[this.state.selectedIndex][1] = Buffer.from(value)
this.setState({members: this.state.members})
this.props.redis.hset(this.state.keyName, key, value, (err, res) => {
this.props.onKeyContentChange()
callback(err, res)
})
} else {
alert('Please wait for data been loaded before saving.')
}
}
load(index) {
if (!super.load(index)) {
return
}
const count = Number(this.cursor) ? 10000 : 500
this.props.redis.hscanBuffer(this.state.keyName, this.cursor, 'MATCH', '*', 'COUNT', count, (_, [cursor, result]) => {
for (let i = 0; i < result.length - 1; i += 2) {
this.state.members.push([result[i].toString(), result[i + 1]])
}
this.cursor = cursor
this.setState({members: this.state.members}, () => {
if (typeof this.state.selectedIndex !== 'number' && this.state.members.length) {
this.handleSelect(null, 0)
}
this.loading = false
if (this.state.members.length - 1 < this.maxRow && Number(cursor)) {
this.load()
}
})
})
}
handleSelect(evt, selectedIndex) {
const item = this.state.members[selectedIndex]
if (item) {
this.setState({selectedIndex, content: item[1]})
}
}
handleKeyDown(e) {
if (typeof this.state.selectedIndex === 'number' && typeof this.state.editableIndex !== 'number') {
if (e.keyCode === 8) {
this.deleteSelectedMember()
return false
}
if (e.keyCode === 38) {
if (this.state.selectedIndex > 0) {
this.handleSelect(null, this.state.selectedIndex - 1)
}
return false
}
if (e.keyCode === 40) {
if (this.state.selectedIndex < this.state.members.length - 1) {
this.handleSelect(null, this.state.selectedIndex + 1)
}
return false
}
}
}
deleteSelectedMember() {
if (typeof this.state.selectedIndex !== 'number') {
return
}
showModal({
title: 'Delete selected item?',
button: 'Delete',
content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
}).then(() => {
const members = this.state.members
const deleted = members.splice(this.state.selectedIndex, 1)
if (deleted.length) {
this.props.redis.hdel(this.state.keyName, deleted[0])
if (this.state.selectedIndex >= members.length - 1) {
this.state.selectedIndex -= 1
}
this.setState({members, length: this.state.length - 1}, () => {
this.props.onKeyContentChange()
this.handleSelect(null, this.state.selectedIndex)
})
}
})
}
showContextMenu(e, row) {
this.handleSelect(null, row)
const menu = remote.Menu.buildFromTemplate([
{
label: 'Copy to Clipboard',
click: () => {
clipboard.writeText(this.state.members[row][0])
}
},
{
type: 'separator'
},
{
label: 'Rename Key',
click: () => {
this.setState({editableIndex: row})
}
},
{
label: 'Delete',
click: () => {
this.deleteSelectedMember()
}
}
])
menu.popup(remote.getCurrentWindow())
}
render() {
return (
{
this.handleSelect(evt, index)
this.setState({editableIndex: index})
}}
width={this.props.contentBarWidth}
height={this.props.height}
headerHeight={24}
>
{
showModal({
button: 'Insert Member',
form: {
type: 'object',
properties: {
'Key:': {
type: 'string'
}
}
}
}).then(res => {
const data = res['Key:']
const value = 'New Member'
this.props.redis.hsetnx(this.state.keyName, data, value).then(inserted => {
if (!inserted) {
alert('The field already exists')
return
}
this.state.members.push([data, Buffer.from(value)])
this.setState({
members: this.state.members,
length: this.state.length + 1
}, () => {
this.props.onKeyContentChange()
this.handleSelect(null, this.state.members.length - 1)
})
})
})
}}
/>
}
width={this.props.contentBarWidth}
cell={({rowIndex}) => {
const member = this.state.members[rowIndex]
if (!member) {
this.load(rowIndex)
return 'Loading...'
}
return ( {
const members = this.state.members
const member = members[rowIndex]
const keyName = this.state.keyName
const source = member[0]
if (source !== target && target) {
this.props.redis.hexists(keyName, target).then(exists => {
if (exists) {
return showModal({
title: 'Overwrite the field?',
button: 'Overwrite',
content: `Field "${target}" already exists. Are you sure you want to overwrite this field?`
}).then(() => {
let found
for (let i = 0; i < members.length; i++) {
if (members[i][0] === target) {
found = i
break
}
}
if (typeof found === 'number') {
members.splice(found, 1)
this.setState({length: this.state.length - 1})
}
})
}
}).then(() => {
member[0] = target
this.props.redis.multi()
.hdel(keyName, source)
.hset(keyName, target, member[1]).exec()
this.setState({members})
}).catch(() => {})
}
this.setState({editableIndex: null})
ReactDOM.findDOMNode(this).focus()
}}
html={member[0]}
/>)
}}
/>
)
}
}
export default HashContent
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/ListContent.jsx
================================================
'use strict'
import React from 'react'
import BaseContent from '.'
import SplitPane from 'react-split-pane'
import {Table, Column} from 'fixed-data-table-contextmenu'
import Editor from './Editor'
import SortHeaderCell from './SortHeaderCell'
import AddButton from '../../../AddButton'
import {remote} from 'electron'
class ListContent extends BaseContent {
save(value, callback) {
const {selectedIndex, keyName, desc} = this.state
if (typeof selectedIndex === 'number') {
const members = this.state.members.slice()
members[selectedIndex] = value.toString()
this.setState({members})
this.props.redis.lset(keyName, desc ? -1 - selectedIndex : selectedIndex, value, (err, res) => {
this.props.onKeyContentChange()
callback(err, res)
})
} else {
alert('Please wait for data been loaded before saving.')
}
}
load(index) {
if (!super.load(index)) {
return
}
const {members, length, keyName, desc} = this.state
let from = members.length
let to = Math.min(from === 0 ? 200 : from + 1000, length - 1)
if (to < from) {
this.loading = false
return
}
if (desc) {
[from, to] = [-1 - to, -1 - from]
}
this.props.redis.lrange(keyName, from, to, (_, results) => {
if (this.state.desc !== desc) {
// TODO: use a counter instead to avoid
// cancel multiple loading attempts.
// LIST & ZSET
this.loading = false
return
}
if (desc) {
results.reverse()
}
const diff = to - from + 1 - results.length
this.setState({
members: members.concat(results),
length: length - diff
}, () => {
if (typeof this.state.selectedIndex !== 'number' && this.state.members.length) {
this.handleSelect(null, 0)
}
this.loading = false
if (this.state.members.length - 1 < this.maxRow && !diff) {
this.load()
}
})
})
}
handleSelect(_, selectedIndex) {
if (typeof this.state.members[selectedIndex] === 'undefined') {
this.setState({selectedIndex: null})
} else {
this.setState({selectedIndex})
}
}
async deleteSelectedMember() {
if (typeof this.state.selectedIndex !== 'number') {
return
}
await showModal({
title: 'Delete selected item?',
button: 'Delete',
content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
})
const {selectedIndex, desc, length, keyName} = this.state
const members = this.state.members.slice()
const deleted = members.splice(selectedIndex, 1)
if (deleted.length) {
this.props.redis.lremindex(keyName, desc ? -1 - selectedIndex : selectedIndex)
const nextSelectedIndex = selectedIndex >= members.length - 1
? selectedIndex - 1
: selectedIndex
this.setState({members, length: length - 1}, () => {
this.props.onKeyContentChange()
this.handleSelect(null, nextSelectedIndex)
})
}
}
handleKeyDown(e) {
if (typeof this.state.selectedIndex === 'number') {
switch (e.keyCode) {
case 8:
this.deleteSelectedMember()
return false
case 38:
if (this.state.selectedIndex > 0) {
this.handleSelect(null, this.state.selectedIndex - 1)
}
return false
case 40:
if (this.state.selectedIndex < this.state.members.length - 1) {
this.handleSelect(null, this.state.selectedIndex + 1)
}
return false
}
}
}
showContextMenu(e, row) {
this.handleSelect(null, row)
const menu = remote.Menu.buildFromTemplate([
{
label: 'Delete',
click: () => {
this.deleteSelectedMember()
}
}
])
menu.popup(remote.getCurrentWindow())
}
renderEditor() {
const content = this.state.members[this.state.selectedIndex]
const buffer = typeof content === 'string'
? Buffer.from(content)
: undefined
return
}
renderIndexColumn() {
return this.setState({
desc,
members: [],
selectedIndex: null
})}
desc={this.state.desc}
/>
}
width={this.props.indexBarWidth}
isResizable
cell={({rowIndex}) => {
return { this.state.desc ? this.state.length - 1 - rowIndex : rowIndex }
}}
/>
}
renderValueColumn() {
return {
const res = await showModal({
button: 'Insert Item',
form: {
type: 'object',
properties: {
'Insert To:': {
type: 'string',
enum: ['head', 'tail']
}
}
}
})
const insertToHead = res['Insert To:'] === 'head'
const method = insertToHead ? 'lpush' : 'rpush'
const data = 'New Item'
await this.props.redis[method](this.state.keyName, data)
const members = this.state.members.slice()
members[insertToHead ? 'unshift' : 'push'](data)
this.setState({
members,
length: this.state.length + 1
}, () => {
this.props.onKeyContentChange()
if (insertToHead) {
this.handleSelect(null, 0)
}
})
}}
/>
}
width={this.props.contentBarWidth - this.props.indexBarWidth}
cell={({rowIndex}) => {
const data = this.state.members[rowIndex]
if (typeof data === 'undefined') {
this.load(rowIndex)
return 'Loading...'
}
return {data}
}}
/>
}
render() {
return (
{this.renderIndexColumn()}
{this.renderValueColumn()}
{this.renderEditor()}
)
}
}
export default ListContent
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/SetContent.jsx
================================================
'use strict'
import React from 'react'
import BaseContent from '.'
import SplitPane from 'react-split-pane'
import {Table, Column} from 'fixed-data-table-contextmenu'
import Editor from './Editor'
import AddButton from '../../../AddButton'
import {remote} from 'electron'
require('./index.scss')
class SetContent extends BaseContent {
save(value, callback) {
if (typeof this.state.selectedIndex === 'number') {
const oldValue = this.state.members[this.state.selectedIndex]
const key = this.state.keyName
this.props.redis.sismember(key, value).then(exists => {
if (exists) {
callback(new Error('The value already exists in the set'))
return
}
this.props.redis.multi().srem(key, oldValue).sadd(key, value).exec((err, res) => {
if (!err) {
this.state.members[this.state.selectedIndex] = value.toString()
this.setState({members: this.state.members})
}
this.props.onKeyContentChange()
callback(err, res)
})
})
} else {
alert('Please wait for data been loaded before saving.')
}
}
load(index) {
if (!super.load(index)) {
return
}
const count = Number(this.cursor) ? 10000 : 500
this.props.redis.sscan(this.state.keyName, this.cursor, 'COUNT', count, (_, [cursor, results]) => {
this.cursor = cursor
const length = Number(cursor) ? this.state.length : this.state.members.length + results.length
this.setState({
members: this.state.members.concat(results),
length
}, () => {
if (typeof this.state.selectedIndex !== 'number' && this.state.members.length) {
this.handleSelect(null, 0)
}
this.loading = false
if (this.state.members.length - 1 < this.maxRow && Number(cursor)) {
this.load()
}
})
})
}
handleSelect(evt, selectedIndex) {
const content = this.state.members[selectedIndex]
if (typeof content !== 'undefined') {
this.setState({selectedIndex, content})
}
}
handleKeyDown(e) {
if (typeof this.state.selectedIndex === 'number') {
if (e.keyCode === 8) {
this.deleteSelectedMember()
return false
}
if (e.keyCode === 38) {
if (this.state.selectedIndex > 0) {
this.handleSelect(null, this.state.selectedIndex - 1)
}
return false
}
if (e.keyCode === 40) {
if (this.state.selectedIndex < this.state.members.length - 1) {
this.handleSelect(null, this.state.selectedIndex + 1)
}
return false
}
}
}
deleteSelectedMember() {
if (typeof this.state.selectedIndex !== 'number') {
return
}
showModal({
title: 'Delete selected item?',
button: 'Delete',
content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
}).then(() => {
const members = this.state.members
const deleted = members.splice(this.state.selectedIndex, 1)
if (deleted.length) {
this.props.redis.srem(this.state.keyName, deleted)
if (this.state.selectedIndex >= members.length - 1) {
this.state.selectedIndex -= 1
}
this.setState({members, length: this.state.length - 1}, () => {
this.props.onKeyContentChange()
this.handleSelect(null, this.state.selectedIndex)
})
}
})
}
showContextMenu(e, row) {
this.handleSelect(null, row)
const menu = remote.Menu.buildFromTemplate([
{
label: 'Delete',
click: () => {
this.deleteSelectedMember()
}
}
])
menu.popup(remote.getCurrentWindow())
}
render() {
return (
{
const member = this.state.members[rowIndex]
if (typeof member === 'undefined') {
this.load(rowIndex)
return 'Loading...'
}
return {member}
}}
header={
{
showModal({
button: 'Insert Member',
form: {
type: 'object',
properties: {
'Value:': {
type: 'string'
}
}
}
}).then(res => {
const data = res['Value:']
return this.props.redis.sismember(this.state.keyName, data).then(exists => {
if (exists) {
const error = 'Member already exists'
alert(error)
throw new Error(error)
}
return data
})
}).then(data => {
this.props.redis.sadd(this.state.keyName, data).then(() => {
this.state.members.push(data)
this.setState({
members: this.state.members,
length: this.state.length + 1
}, () => {
this.props.onKeyContentChange()
this.handleSelect(null, this.state.members.length - 1)
})
})
})
}}
/>
}
/>
)
}
}
export default SetContent
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/SortHeaderCell.jsx
================================================
'use strict'
import React, {memo} from 'react'
import {Cell} from 'fixed-data-table-contextmenu'
function SortHeaderCell({onOrderChange, desc, title}) {
function handleOnClick(evt) {
onOrderChange(!desc)
evt.preventDefault()
evt.stopPropagation()
}
return (
{title}
{
}
| )
}
export default memo(SortHeaderCell)
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/StringContent.jsx
================================================
'use strict'
import React from 'react'
import BaseContent from '.'
import Editor from './Editor'
class StringContent extends BaseContent {
init(keyName, keyType) {
super.init(keyName, keyType)
this.props.redis.getBuffer(keyName, (_, buffer) => {
this.setState({buffer: buffer instanceof Buffer ? buffer : Buffer.alloc(0)})
})
}
save(value, callback) {
if (this.state.keyName) {
this.props.redis.setKeepTTL(this.state.keyName, value, (err, res) => {
this.props.onKeyContentChange()
callback(err, res)
})
} else {
alert('Please wait for data been loaded before saving.')
}
}
create() {
return this.props.redis.set(this.state.keyName, '')
}
render() {
return ( )
}
}
export default StringContent
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/ZSetContent.jsx
================================================
'use strict'
import React from 'react'
import BaseContent from '.'
import SplitPane from 'react-split-pane'
import {Table, Column} from 'fixed-data-table-contextmenu'
import Editor from './Editor'
import SortHeaderCell from './SortHeaderCell'
import AddButton from '../../../AddButton'
import ContentEditable from '../../../ContentEditable'
import ReactDOM from 'react-dom'
import {clipboard, remote} from 'electron'
import sortedIndexBy from 'lodash.sortedindexby'
require('./index.scss')
class ZSetContent extends BaseContent {
save(value, callback) {
const {selectedIndex, members, keyName} = this.state
if (typeof selectedIndex === 'number') {
const item = members[selectedIndex]
const oldValue = item[0]
item[0] = value.toString()
this.setState({members})
this.props.redis.multi()
.zrem(keyName, oldValue)
.zadd(keyName, item[1], value)
.exec((err, res) => {
this.props.onKeyContentChange()
callback(err, res)
})
} else {
alert('Please wait for data been loaded before saving.')
}
}
load(index) {
if (!super.load(index)) {
return
}
const {members, desc, length, keyName} = this.state
const from = members.length
const to = Math.min(from === 0 ? 200 : from + 1000, length - 1)
const commandName = desc ? 'zrevrange' : 'zrange'
this.props.redis[commandName](keyName, from, to, 'WITHSCORES', (_, results) => {
if (this.state.desc !== desc || this.state.members.length !== from) {
this.loading = false
return
}
const items = []
for (let i = 0; i < results.length - 1; i += 2) {
items.push([results[i], results[i + 1]])
}
const diff = to - from + 1 - items.length
this.setState({
members: members.concat(items),
length: length - diff
}, () => {
const currentMembers = this.state.members
if (typeof this.state.selectedIndex !== 'number' && currentMembers.length) {
this.handleSelect(null, 0)
}
this.loading = false
if (currentMembers.length - 1 < this.maxRow && !diff) {
this.load()
}
})
})
}
handleSelect(_, selectedIndex) {
const item = this.state.members[selectedIndex]
if (item) {
this.setState({selectedIndex})
}
}
handleKeyDown(e) {
const {selectedIndex, editableIndex, members} = this.state
if (typeof selectedIndex === 'number' && typeof editableIndex !== 'number') {
switch (e.keyCode) {
case 8:
this.deleteSelectedMember()
return false
case 38:
if (selectedIndex > 0) {
this.handleSelect(null, selectedIndex - 1)
}
return false
case 40:
if (selectedIndex < members.length - 1) {
this.handleSelect(null, selectedIndex + 1)
}
return false
}
}
}
deleteSelectedMember() {
if (typeof this.state.selectedIndex !== 'number') {
return
}
showModal({
title: 'Delete selected item?',
button: 'Delete',
content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
}).then(() => {
const members = this.state.members
const deleted = members.splice(this.state.selectedIndex, 1)
if (deleted.length) {
this.props.redis.zrem(this.state.keyName, deleted[0])
const nextSelectedIndex = this.state.selectedIndex >= members.length - 1
? this.state.selectedIndex - 1
: this.state.selectedIndex
this.setState({members, length: this.state.length - 1}, () => {
this.props.onKeyContentChange()
this.handleSelect(null, nextSelectedIndex)
})
}
})
}
showContextMenu(_, row) {
this.handleSelect(null, row)
const menu = remote.Menu.buildFromTemplate([
{
label: 'Copy Score to Clipboard',
click: () => {
clipboard.writeText(this.state.members[row][1])
}
},
{
type: 'separator'
},
{
label: 'Edit Score',
click: () => {
this.setState({editableIndex: row})
}
},
{
label: 'Delete',
click: () => {
this.deleteSelectedMember()
}
}
])
menu.popup(remote.getCurrentWindow())
}
renderTable() {
return {
this.handleSelect(evt, index)
this.setState({editableIndex: index})
}}
isColumnResizing={false}
onColumnResizeEndCallback={this.props.setSize.bind(null, 'score')}
width={this.props.contentBarWidth}
height={this.props.height}
headerHeight={24}
>
{this.renderScoreColumn()}
{this.renderMemberColumn()}
}
renderScoreColumn() {
return this.setState({
desc,
members: [],
selectedIndex: null
})}
desc={this.state.desc}
/>
}
width={this.props.scoreBarWidth}
isResizable
cell={({rowIndex}) => {
const member = this.state.members[rowIndex]
if (!member) {
return ''
}
return ( {
const members = this.state.members.slice()
try {
await this.props.redis.zadd(this.state.keyName, newScore, members[rowIndex][0])
// Don't sort when changing scores
members[rowIndex][1] = newScore
this.setState({
members,
editableIndex: null
})
} catch (err) {
alert(err.message)
this.setState({
editableIndex: null
})
}
ReactDOM.findDOMNode(this.refs.table).focus()
}}
html={member[1]}
/>)
}}
/>
}
renderMemberColumn() {
return {
const res = await showModal({
button: 'Insert Member',
form: {
type: 'object',
properties: {
'Value:': {
type: 'string'
},
'Score:': {
type: 'number'
}
}
}
})
const data = res['Value:']
const score = res['Score:']
const rank = await this.props.redis.zscore(this.state.keyName, data)
if (rank !== null) {
const error = 'Member already exists'
alert(error)
return
}
await this.props.redis.zadd(this.state.keyName, score, data)
const members = this.state.members.slice()
const newMember = [data, score]
const index = sortedIndexBy(
members,
newMember,
(member) => Number(member[1]) * (this.state.desc ? -1 : 1)
)
if (index < members.length - 1) {
members.splice(index, 0, newMember)
this.setState({
members,
length: this.state.length + 1
}, () => {
this.props.onKeyContentChange()
this.handleSelect(null, index)
})
}
alert('Added successfully')
}}/>
}
width={this.props.contentBarWidth - this.props.scoreBarWidth}
cell={({rowIndex}) => {
const member = this.state.members[rowIndex]
if (!member) {
this.load(rowIndex)
return 'Loading...'
}
return {member[0]}
}}
/>
}
renderEditor() {
const item = this.state.members[this.state.selectedIndex]
const buffer = item
? Buffer.from(item[0])
: undefined
return
}
render() {
return (
{this.renderTable()}
{this.renderEditor()}
)
}
}
export default ZSetContent
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/index.jsx
================================================
'use strict'
import React from 'react'
require('./index.scss')
const getDefaultState = function () {
return {
keyName: null,
content: null,
desc: false,
length: 0,
members: []
}
}
class BaseContent extends React.Component {
constructor() {
super()
this.state = getDefaultState()
this.maxRow = 0
this.cursor = 0
this.randomClass = 'base-content-' + (Math.random() * 100000 | 0)
}
init(keyName, keyType) {
if (!keyName || !keyType) {
return
}
this.loading = false
this.setState(getDefaultState())
const {redis} = this.props
const method = {
string: 'strlen',
list: 'llen',
set: 'scard',
zset: 'zcard',
hash: 'hlen'
}[keyType]
redis[method](keyName).then(length => {
this.setState({keyName, length: length || 0})
})
}
load(index) {
if (index > this.maxRow) {
this.maxRow = index
}
if (this.loading) {
return
}
this.loading = true
return true
}
rowClassGetter(index) {
const item = this.state.members[index]
if (typeof item === 'undefined') {
return 'type-list is-loading'
}
if (index === this.state.selectedIndex) {
return 'type-list is-selected'
}
return 'type-list'
}
componentDidMount() {
this.init(this.props.keyName, this.props.keyType)
}
componentDidUpdate() {
if (typeof this.state.scrollToRow === 'number') {
this.setState({scrollToRow: null})
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.keyName !== this.props.keyName ||
nextProps.keyType !== this.props.keyType) {
this.init(nextProps.keyName, nextProps.keyType)
}
}
componentWillUnmount() {
this.setState = function () {}
}
}
export default BaseContent
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/index.scss
================================================
.BaseContent {
flex: 1;
position: relative;
.type-list {
.index-label {
background: #ccc;
margin: 4px 4px 0 0;
font-family: Consolas, monospace !important;
padding: 0 4px !important;
height: 16px;
font-size: 11px !important;
line-height: 16px !important;
display: block;
text-align: center;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.SortHeaderCell {
position: relative;
a {
display: block;
}
img {
position: absolute;
right: -15px;
top: 5px;
}
&.is-asc img {
transform: rotate(180deg);
}
}
.base-content {
margin-top: -1px;
position: relative;
overflow: hidden;
&:focus {
outline: 0;
}
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/index.jsx
================================================
'use strict'
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {setSize} from 'Redux/actions'
import StringContent from './BaseContent/StringContent'
import ListContent from './BaseContent/ListContent'
import SetContent from './BaseContent/SetContent'
import HashContent from './BaseContent/HashContent'
import ZSetContent from './BaseContent/ZSetContent'
require('./index.scss')
class KeyContent extends PureComponent {
constructor() {
super()
this.state = {}
}
render() {
const props = {key: this.props.keyName, ...this.props}
let view
switch (this.props.keyType) {
case 'string': view = ; break
case 'list': view = ; break
case 'set': view = ; break
case 'hash': view = ; break
case 'zset': view = ; break
case 'none':
view = ()
break
}
return { view }
}
}
function mapStateToProps(state) {
return {
contentBarWidth: state.sizes.get('contentBarWidth') || 200,
scoreBarWidth: state.sizes.get('scoreBarWidth') || 60,
indexBarWidth: state.sizes.get('indexBarWidth') || 60
}
}
const mapDispatchToProps = {
setSize
}
export default connect(mapStateToProps, mapDispatchToProps)(KeyContent)
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/index.scss
================================================
.notfound {
position: absolute;
top: 50%;
font-size: 22px;
color: #ccc;
text-align: center;
transform: translateY(-50%);
width: 100%;
p {
margin: 0;
}
.icon {
font-size: 62px;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/TabBar/index.jsx
================================================
'use strict'
import React, {memo} from 'react'
require('./index.scss')
const TABS = ['Content', 'Terminal', 'Config']
function renderTabIcon(tab) {
switch (tab) {
case 'Content':
return
case 'Terminal':
return
case 'Config':
return
}
}
function renderTab(tab, {activeTab, onSelectTab}) {
return onSelectTab(tab)}
>
{renderTabIcon(tab)}
{tab}
}
function Content(props) {
return {TABS.map(tab => renderTab(tab, props))}
}
export default memo(Content)
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/TabBar/index.scss
================================================
.TabBar {
text-align: right;
border-bottom: 1px solid #d3d3d3;
.item {
display: inline-block;
padding: 12px;
.icon {
margin-right: 5px;
}
&.is-active {
background: #dbdfe1;
}
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Terminal/index.jsx
================================================
'use strict'
import React from 'react'
import commands from 'redis-commands'
import splitargs from 'redis-splitargs'
import 'jquery.terminal'
require('../../../../../../../../node_modules/jquery.terminal/css/jquery.terminal.css')
require('./index.scss')
class Terminal extends React.PureComponent {
constructor() {
super()
this.onSelectBinded = this.onSelect.bind(this)
}
componentDidMount() {
const {redis} = this.props
redis.on('select', this.onSelectBinded)
const terminal = this.terminal = $(this.refs.terminal).terminal((command, term) => {
if (!command) {
return
}
command = splitargs(command)
const commandName = command[0] && command[0].toUpperCase()
if (commandName === 'FLUSHALL' || commandName === 'FLUSHDB') {
term.push(input => {
if (input.match(/y|yes/i)) {
this.execute(term, command)
term.pop()
} else if (input.match(/n|no/i)) {
term.pop()
}
}, {
prompt: '[[;#aac6e3;]Are you sure (y/n)? ]'
})
} else {
this.execute(term, command)
}
}, {
greetings: '',
exit: false,
completion(command, callback) {
const commandName = command.split(' ')[0]
const lower = commandName.toLowerCase()
const isUppercase = commandName.toUpperCase() === commandName
callback(
commands.list
.filter(item => item.indexOf(lower) === 0)
.map(item => {
const last = item.slice(commandName.length)
return commandName + (isUppercase ? last.toUpperCase() : last)
})
)
},
name: this.props.connectionKey,
height: '100%',
width: '100%',
outputLimit: 200,
prompt: `[[;#fff;]redis> ]`,
keydown(e) {
if (!terminal.enabled()) {
return true
}
if (e.ctrlKey || e.metaKey) {
if (e.keyCode >= 48 && e.keyCode <= 57) {
return true
}
if ([84, 87, 78, 82, 81].indexOf(e.keyCode) !== -1) {
return true
}
}
if (e.ctrlKey) {
if (e.keyCode === 67) {
if (terminal.level() > 1) {
terminal.pop()
if (terminal.paused()) {
terminal.resume()
}
}
return false
}
}
}
})
}
onSelect(db) {
this.props.onDatabaseChange(db)
}
execute(term, args) {
term.pause()
const redis = this.props.redis
if (args.length === 1 && args[0].toUpperCase() === 'MONITOR') {
redis.monitor((_, monitor) => {
term.echo('[[;#aac6e3;]Enter monitor mode. Press Ctrl+C to exit. ]')
term.resume()
term.push(input => {
}, {
onExit() {
monitor.disconnect()
}
})
monitor.on('monitor', (time, args) => {
if (term.level() > 1) {
term.echo(formatMonitor(time, args), {raw: true})
}
})
})
} else if (args.length > 1 && ['SUBSCRIBE', 'PSUBSCRIBE'].indexOf(args[0].toUpperCase()) !== -1) {
const newRedis = redis.duplicate()
newRedis.call.apply(newRedis, args).then(res => {
term.echo('[[;#aac6e3;]Enter subscription mode. Press Ctrl+C to exit. ]')
term.resume()
term.push(input => {
}, {
prompt: '',
onExit() {
newRedis.disconnect()
}
})
})
newRedis.on('message', (channel, message) => {
term.echo(formatMessage(channel, message), {raw: true})
})
newRedis.on('pmessage', (pattern, channel, message) => {
term.echo(formatMessage(channel, message), {raw: true})
})
} else {
redis.call.apply(redis, args).then(res => {
term.echo(getHTML(res), {raw: true})
term.resume()
}).catch(err => {
term.echo(getHTML(err), {raw: true})
term.resume()
})
}
}
componentWillReceiveProps(nextProps) {
if (this.props.style.display === 'none' && nextProps.style.display === 'block') {
this.terminal.focus()
}
}
componentWillUnmount() {
this.props.redis.removeAllListeners('select', this.onSelectBinded)
}
render() {
return (
)
}
}
export default Terminal
function getHTML(response) {
if (Array.isArray(response)) {
return `
${response.map((item, index) => '' + index + ' ' + getHTML(item) + ' ').join('')}
`
}
const type = typeof response
if (type === 'number') {
return `${response}
`
}
if (type === 'string') {
return `${response.replace(/\r?\n/g, ' ')}
`
}
if (response === null) {
return `null
`
}
if (response instanceof Error) {
return `${response.message}
`
}
if (type === 'object') {
return `
${Object.keys(response).map(item => '' + item + ' ' + getHTML(response[item]) + ' ').join('')}
`
}
return `${JSON.stringify(response)}
`
}
function formatMonitor(time, args) {
args = args || []
const command = args[0] ? args.shift().toUpperCase() : ''
if (command) {
commands.getKeyIndexes(command.toLowerCase(), args).forEach(index => {
args[index] = `${args[index]} `
})
}
return `
${time}
${command}
${args.join(' ')}
`
}
function formatMessage(channel, message) {
return `
${channel}
${message}
`
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Terminal/index.scss
================================================
.Terminal {
overflow: auto;
flex: 1;
* {
-webkit-user-select: text;
}
&.terminal, .cmd {
--background: #272b34;
--background: #000;
--size: 1.4;
font-family: Consolas, monospace;
}
.number {
color: #78CF8A;
}
.string {
color: #d6ec9c;
}
.array-resp, .object-resp {
margin: 0;
padding: 0;
li {
display: flex;
span {
color: #848080;
min-width: 28px;
text-align: right;
margin-right: 10px;
}
div {
flex: 1;
}
}
}
.object-resp li span {
font-weight: bold;
color: #cda869;
}
.null {
color: #cf7ea9;
}
.error {
color: #ee6868 !important;
}
.list {
color: #8f9d6a;
}
.monitor {
color: #8f9d6a;
.time {
color: #3d3d3d;
margin-right: 4px;
}
.command-name {
color: #cf7ea9;
}
.command-key {
color: #cda869;
}
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/index.jsx
================================================
'use strict'
import React from 'react'
import TabBar from './TabBar'
import KeyContent from './KeyContent'
import Terminal from './Terminal'
import Config from './Config'
import Footer from './Footer'
class Content extends React.PureComponent {
constructor() {
super()
this.state = {
pattern: '',
db: 0,
version: 0,
tab: 'Content'
}
}
init(keyName) {
this.setState({keyType: null})
if (keyName !== null) {
this.setState({keyType: null})
this.props.redis.type(keyName).then(keyType => {
if (keyName === this.props.keyName) {
this.setState({keyType})
}
})
}
}
componentDidMount() {
this.init(this.props.keyName)
}
componentWillReceiveProps(nextProps) {
if (nextProps.keyName !== this.props.keyName || nextProps.version !== this.props.version) {
this.init(nextProps.keyName)
}
if (nextProps.metaVersion !== this.props.metaVersion) {
this.setState({version: this.state.version + 1})
}
}
handleTabChange(tab) {
this.setState({tab})
}
render() {
return (
{
this.setState({version: this.state.version + 1})
}}
/>
)
}
}
export default Content
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/ContentEditable/index.jsx
================================================
import React from 'react'
import ReactDOM from 'react-dom'
import escape from 'lodash.escape'
require('./index.scss')
export default class ContentEditable extends React.Component {
constructor() {
super()
}
render() {
const {html, enabled, ...props} = this.props
return (
)
}
shouldComponentUpdate(nextProps) {
return nextProps.html !== this.props.html || // ReactDOM.findDOMNode(this.refs.text).innerHTML ||
nextProps.enabled !== this.props.enabled
}
componentDidMount() {
if (this.props.enabled) {
ReactDOM.findDOMNode(this.refs.text).focus()
}
}
componentDidUpdate() {
const node = ReactDOM.findDOMNode(this.refs.text)
if (escape(this.props.html) !== node.innerHTML) {
node.innerHTML = this.props.html
}
if (this.props.enabled) {
const range = document.createRange()
range.selectNodeContents(node)
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
}
}
handleKeyDown(evt) {
if (evt.keyCode === 13) {
ReactDOM.findDOMNode(this.refs.text).blur()
evt.preventDefault()
evt.stopPropagation()
return
}
if (evt.keyCode === 27) {
this.props.onChange(this.props.html)
evt.preventDefault()
evt.stopPropagation()
}
}
handleChange(evt) {
const html = ReactDOM.findDOMNode(this.refs.text).innerHTML
if (html !== this.lastHtml) {
evt.target = {value: html}
}
this.lastHtml = html
}
handleSubmit() {
this.props.onChange(ReactDOM.findDOMNode(this.refs.text).textContent)
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/ContentEditable/index.scss
================================================
.ContentEditable {
[contenteditable="true"] {
background: #fff !important;
color: #333;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/Footer.jsx
================================================
'use strict'
import React from 'react'
class Footer extends React.Component {
constructor() {
super()
this.state = {}
}
componentDidMount() {
this.updateInfo()
this.updateDBCount()
this.interval = setInterval(this.updateInfo.bind(this), 10000)
}
componentWillReceiveProps(nextProps) {
if (nextProps.db !== this.props.db) {
this.updateInfo()
}
}
updateDBCount() {
this.props.redis.config('get', 'databases', (err, res) => {
if (!err && res[1]) {
this.setState({databases: Number(res[1])})
} else {
const redis = this.props.redis.duplicate()
const select = redis.select.bind(redis)
this.guessDatabaseNumber(select, 15).then(count => {
return typeof count === 'number' ? count : this.guessDatabaseNumber(select, 1, 0)
}).then(count => {
this.setState({databases: count + 1})
})
}
})
}
updateInfo() {
this.props.redis.info((err, res) => {
if (err) {
return
}
const info = {}
const lines = res.split('\r\n')
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(':')
if (parts[1]) {
info[parts[0]] = parts[1]
}
}
this.setState(info)
})
}
guessDatabaseNumber(select, startIndex, lastSuccessIndex) {
if (startIndex > 30) {
return Promise.resolve(30)
}
return select(startIndex)
.then(() => {
return this.guessDatabaseNumber(select, startIndex + 1, startIndex)
}).catch(err => {
if (typeof lastSuccessIndex === 'number') {
return lastSuccessIndex
}
return null
})
}
componentWillUnmount() {
clearInterval(this.interval)
this.interval = null
}
handleChange(evt) {
const db = Number(evt.target.value)
this.props.onDatabaseChange(db)
}
keyCountByDb(dbNumber){
const db = `db${dbNumber}`
let keys = 0
if (this.state[db]) {
const match = this.state[db].match(/keys=(\d+)/)
if (match) {
keys = match[1]
}
}
return keys
}
render() {
const keys = this.keyCountByDb(this.props.db)
return ()
}
}
export default Footer
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/KeyList/index.jsx
================================================
'use strict'
import React from 'react'
import ReactDOM from 'react-dom'
import {Table, Column} from 'fixed-data-table-contextmenu'
import ContentEditable from '../../ContentEditable'
import AddButton from '../../AddButton'
import zip from 'lodash.zip'
import {clipboard, remote} from 'electron'
require('./index.scss')
class KeyList extends React.Component {
state = {
keys: [],
selectedKey: null,
sidebarWidth: 300,
cursor: '0'
}
randomClass = 'pattern-table-' + (Math.random() * 100000 | 0)
refresh(firstTime) {
this.setState({
cursor: '0',
keys: []
}, () => {
this.handleSelect()
this.scan(firstTime)
})
}
componentWillReceiveProps(nextProps) {
if (nextProps.db !== this.props.db) {
this.props.redis.select(nextProps.db)
}
const needRefresh = nextProps.db !== this.props.db ||
nextProps.pattern !== this.props.pattern ||
nextProps.redis !== this.props.redis
if (needRefresh) {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
this.timer = setTimeout(() => {
this.refresh(true)
}, 200)
}
}
scan(firstTime) {
const scanKey = this.scanKey = Math.random() * 10000 | 0
if (this.scanning) {
this.lastFirstTime = firstTime
return
}
this.scanning = true
this.setState({scanning: true})
const redis = this.props.redis
const targetPattern = this.props.pattern
let pattern = targetPattern
if (pattern.indexOf('*') === -1 && pattern.indexOf('?') === -1) {
pattern += '*'
}
let count = 0
let cursor = this.state.cursor
let filterKey
let filterKeyExists
// Plain key
if (targetPattern !== pattern) {
filterKey = targetPattern
if (this.state.keys.length) {
iter.call(this, 100, 1)
} else {
redis.type(targetPattern, (err, type) => {
if (type !== 'none') {
this.setState({
keys: this.state.keys.concat([[targetPattern, type]])
})
filterKeyExists = true
if (firstTime) {
iter.call(this, 1, 1)
return
}
}
iter.call(this, 100, 1)
})
}
} else {
iter.call(this, 100, 1)
}
function iter(fetchCount, times) {
redis.scan(cursor, 'MATCH', pattern, 'COUNT', fetchCount, (err, res) => {
if (this.scanKey !== scanKey) {
this.scanning = false
setTimeout(this.scan.bind(this, this.lastFirstTime), 0)
return
}
const newCursor = res[0]
let fetchedKeys = res[1]
let promise
if (fetchedKeys.length) {
if (filterKey) {
fetchedKeys = fetchedKeys.filter(key => key !== filterKey)
}
count += fetchedKeys.length
const pipeline = redis.pipeline()
fetchedKeys.forEach(key => pipeline.type(key))
promise = pipeline.exec()
} else {
promise = Promise.resolve([])
}
promise.then(types => {
if (this.props.pattern !== targetPattern) {
this.scanning = false
setTimeout(this.scan.bind(this), 0)
return
}
const keys = zip(fetchedKeys, types.map(res => res[1]))
let needContinue = true
if (filterKeyExists && firstTime) {
needContinue = false
} else if (Number(newCursor) === 0) {
needContinue = false
} else if (count >= 100) {
needContinue = false
} else if (count > 0 && times > 200) {
needContinue = false
}
cursor = newCursor
if (needContinue) {
this.setState({
cursor,
keys: this.state.keys.concat(keys)
}, () => {
iter.call(this, count < 10 ? 5000 : (count < 50 ? 2000 : 1000), times + 1)
if (typeof this.index !== 'number') {
this.handleSelect(0)
}
})
} else {
this.setState({
cursor,
scanning: false,
keys: this.state.keys.concat(keys)
}, () => {
this.scanning = false
if (typeof this.index !== 'number') {
this.handleSelect(0)
}
})
}
})
})
}
}
handleSelect(index, force) {
if (index === this.index && !force) {
return
}
const item = this.state.keys[index]
if (item && typeof item[0] !== 'undefined') {
const key = item[0]
this.index = index
const editableKey = this.state.editableKey === key ? this.state.editableKey : null
this.setState({selectedKey: item[0], editableKey})
this.props.onSelect(item[0])
} else {
this.index = null
this.setState({selectedKey: null, editableKey: null})
this.props.onSelect(null)
}
}
deleteSelectedKey() {
if (typeof this.index !== 'number') {
return
}
showModal({
title: 'Delete selected key?',
button: 'Delete',
content: 'Are you sure you want to delete the selected key? This action cannot be undone.'
}).then(() => {
const keys = this.state.keys
const deleted = keys.splice(this.index, 1)
if (deleted.length) {
this.props.redis.del(deleted[0][0])
if (this.index >= keys.length - 1) {
this.index -= 1
}
this.setState({keys}, () => {
this.handleSelect(this.index, true)
})
}
}).catch(() => {})
}
componentDidMount() {
$(ReactDOM.findDOMNode(this)).on('keydown', e => {
if (typeof this.index === 'number' && typeof this.state.editableKey !== 'string') {
if (e.keyCode === 8) {
this.deleteSelectedKey()
return false
}
if (e.keyCode === 38) {
this.handleSelect(this.index - 1)
return false
}
if (e.keyCode === 40) {
this.handleSelect(this.index + 1)
return false
}
}
if (!e.ctrlKey && e.metaKey) {
if (e.keyCode === 67) {
clipboard.writeText(this.state.keys[this.index][0])
return false
}
if (e.keyCode === 82) {
this.refresh()
return false
}
}
return true
})
this.scan()
}
setTTLforKey() {
const {redis, onKeyMetaChange} = this.props
redis.pttl(this.state.selectedKey).then(ttl => {
showModal({
button: 'Set Expiration',
form: {
type: 'object',
properties: {
'PTTL (ms):': {
type: 'number',
minLength: 1,
default: ttl
}
}
}
}).then(res => {
const ttl = Number(res['PTTL (ms):'])
if (ttl >= 0) {
redis.pexpire(this.state.selectedKey, ttl).then(res => {
if (res <= 0) {
alert('Update Failed')
}
onKeyMetaChange()
})
} else {
redis.persist(this.state.selectedKey, () => {
onKeyMetaChange()
})
}
})
})
}
duplicateKey() {
const sourceKey = this.state.keys[this.index][0]
let targetKey
showModal({
button: 'Duplicate Key',
form: {
type: 'object',
properties: {
'Target Key:': {
type: 'string',
minLength: 1
},
'Keep TTL:': {
type: 'boolean'
}
}
}
}).then(res => {
targetKey = res['Target Key:']
const duplicateTTL = res['Keep TTL:']
this.props.redis.duplicateKey(sourceKey, targetKey, duplicateTTL ? 'TTL' : 'NOTTL')
}).then(() => {
this.props.onCreateKey(targetKey)
}).catch(err => {
if (err && err.message) {
alert(err.message)
}
})
}
createKey(key, type) {
const redis = this.props.redis
switch (type) {
case 'string':
return redis.set(key, '')
case 'list':
return redis.lpush(key, 'New Item')
case 'hash':
return redis.hset(key, 'New Key', 'New Value')
case 'set':
return redis.sadd(key, 'New Member')
case 'zset':
return redis.zadd(key, 0, 'New Member')
}
}
showContextMenu(e, row) {
this.handleSelect(row)
const menu = remote.Menu.buildFromTemplate([
{
label: 'Copy to Clipboard',
click: () => {
clipboard.writeText(this.state.keys[row][0])
}
},
{
label: 'Reload',
click: () => {
this.handleSelect(row, true)
}
},
{
type: 'separator'
},
{
label: 'Set expiration',
click: () => {
this.setTTLforKey()
}
},
{
label: 'Rename Key...',
click: () => {
this.setState({editableKey: this.state.keys[row][0]})
}
},
{
label: 'Duplicate Key...',
click: () => {
this.duplicateKey()
}
},
{
label: 'Delete',
click: () => {
this.deleteSelectedKey()
}
}
])
menu.popup(remote.getCurrentWindow())
}
render() {
return (
{
if (this.state.editableKey) {
this.setState({editableKey: null})
}
}}
rowClassNameGetter={index => {
const item = this.state.keys[index]
if (!item) {
return 'is-loading'
}
if (item[0] === this.state.selectedKey) {
return 'is-selected'
}
return ''
}}
onRowContextMenu={this.showContextMenu.bind(this)}
onRowClick={(evt, index) => this.handleSelect(index)}
onRowDoubleClick={(evt, index) => {
this.handleSelect(index)
this.setState({editableKey: this.state.keys[index][0]})
}}
width={this.props.width}
height={this.props.height}
headerHeight={24}
>
{
const item = this.state.keys[rowIndex]
if (!item) {
return ''
}
const cellData = item[1]
if (!cellData) {
return ''
}
const type = cellData === 'string' ? 'str' : cellData
return {type}
}}
/>
{
this.refresh()
}} onClick={() => {
showModal({
button: 'Create Key',
form: {
type: 'object',
properties: {
'Key Name:': {
type: 'string',
minLength: 1
},
'Type:': {
type: 'string',
enum: ['string', 'hash', 'list', 'set', 'zset']
}
}
}
}).then(res => {
const key = res['Key Name:']
const type = res['Type:']
return this.props.redis.exists(key).then(exists => {
const error = 'The key already exists'
if (exists) {
alert(error)
throw new Error(error)
}
return {key, type}
})
}).then(({key, type}) => {
this.createKey(key, type).then(() => {
this.props.onCreateKey(key)
})
})
}}
/>
}
width={this.props.width - 40}
cell={({rowIndex}) => {
const item = this.state.keys[rowIndex]
let cellData
if (item) {
cellData = item[0]
}
if (typeof cellData === 'undefined') {
if (this.state.scanning) {
return Scanning...(cursor {this.state.cursor})
}
return ( {
evt.preventDefault()
this.scan()
}}>Scan more )
}
return ( {
const keys = this.state.keys
const oldKey = keys[rowIndex][0]
if (oldKey !== newKeyName && newKeyName) {
this.props.redis.exists(newKeyName).then(exists => {
if (exists) {
return showModal({
title: 'Overwrite the key?',
button: 'Overwrite',
content: `Key "${newKeyName}" already exists. Are you sure you want to overwrite this key?`
})
}
}).then(() => {
keys[rowIndex] = [newKeyName, keys[rowIndex][1]]
this.props.redis.rename(oldKey, newKeyName)
let found
for (let i = 0; i < keys.length; i++) {
if (i !== rowIndex && keys[i][0] === newKeyName) {
keys.splice(i, 1)
found = i
break
}
}
if (typeof found === 'number') {
if (this.index >= found) {
this.index -= 1
}
this.setState({keys}, () => {
this.handleSelect(this.index, true)
})
} else {
this.setState({keys})
}
}).catch(() => {})
}
this.setState({editableKey: null})
ReactDOM.findDOMNode(this).focus()
}}
html={cellData}
/>)
}}
/>
)
}
}
export default KeyList
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/KeyList/index.scss
================================================
.pattern-table {
position: relative;
overflow: hidden;
&:focus {
outline: 0;
}
footer {
height: 24px;
}
}
.public_fixedDataTable_bottomShadow {
display: none;
}
.key-type {
margin: 4px 0 0;
padding: 0 !important;
text-transform: uppercase;
width: 32px;
height: 16px;
font-size: 11px !important;
line-height: 17px !important;
display: block;
text-align: center;
background: #60d4ca;
color: #fff;
&.str { background: #5dc936; }
&.list { background: #fca32a; }
&.hash { background: #b865d0; }
&.zset { background: #fa5049; }
&.set { background: #239ff2; }
}
.public_fixedDataTable_header, .public_fixedDataTableRow_main.is-loading {
.public_fixedDataTableCell_main {
font-family: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif !important;
}
}
.public_fixedDataTableCell_cellContent {
padding: 0;
}
.public_fixedDataTableCell_main {
font-family: Consolas, monospace;
font-size: 12px;
line-height: 24px;
padding: 0 8px;
}
.public_fixedDataTableRow_main {
color: #606061;
}
:focus .public_fixedDataTableRow_main.is-selected {
background: #116cd6;
color: #fff;
.public_fixedDataTableCell_main {
background: transparent;
}
}
.public_fixedDataTableRow_main.is-selected {
background: #dcdcdc;
.public_fixedDataTableCell_main {
background: transparent;
}
}
.public_fixedDataTableCell_main {
border: none;
}
.public_fixedDataTable_main {
border-right: none;
border-left: none;
border-bottom: none;
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/PatternList/index.jsx
================================================
'use strict'
import React from 'react'
import {ipcRenderer} from 'electron'
require('./index.scss')
class PatternList extends React.Component {
constructor(props) {
super()
this.state = {
patternDropdown: false,
pattern: props.pattern
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.db !== this.props.db) {
this.updatePattern('')
}
if (nextProps.pattern !== this.props.pattern) {
this.setState({pattern: nextProps.pattern})
}
}
updatePattern(value) {
this.setState({pattern: value})
this.props.onChange(value)
}
render() {
return ()
}
}
export default PatternList
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/PatternList/index.scss
================================================
.pattern-input {
position: relative;
padding: 6px;
.icon-search {
position: absolute;
left: 14px;
top: 12px;
opacity: 0.5;
}
.icon-down-open {
position: absolute;
right: 0;
top: 0;
opacity: 0.5;
transition: 0.1s;
display: inline-block;
width: 40px;
text-align: center;
height: 42px;
line-height: 42px;
&.is-active {
transform: rotate(180deg);
}
}
input {
padding-left: 22px;
}
}
.pattern-dropdown {
position: absolute;
z-index: 999;
background: #fff;
margin-top: 6px;
left: 0;
width: 100%;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.21);
transition: transform 125ms cubic-bezier(0.18, 0.89, 0.32, 1.12), opacity 100ms linear;
transform-origin: top;
transform: scale(1, 0.2);
pointer-events: none;
opacity: 0;
&.is-active {
pointer-events: initial;
opacity: 1;
transform: scale(1, 1);
}
ul {
height: 100%;
overflow: auto;
}
li {
display: block;
padding: 8px 12px;
font-family: Consolas, monospace;
border-top: 1px solid #f5f5f4;
&:hover {
background: #116cd6;
color: #fff;
}
&:last-child {
font-family: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif;
}
}
}
.manage-pattern-button {
color: #116cd6;
span.icon {
margin-right: 5px;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/index.jsx
================================================
'use strict'
import React, {memo} from 'react'
import {List} from 'immutable'
import PatternList from './PatternList'
import KeyList from './KeyList'
import Footer from './Footer'
const FOOTER_HEIGHT = 66
function KeyBrowser({
pattern, patterns, connectionKey, db, height, width, redis,
onPatternChange, onCreateKey, onKeyMetaChange, onSelectKey, onDatabaseChange
}) {
const clientHeight = height - FOOTER_HEIGHT
return ()
}
export default memo(KeyBrowser)
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/index.jsx
================================================
'use strict'
import React from 'react'
import {connect} from 'react-redux'
import SplitPane from 'react-split-pane'
import KeyBrowser from './KeyBrowser'
import Content from './Content'
require('./index.scss')
class Database extends React.PureComponent {
constructor() {
super()
this.$window = $(window)
this.state = {
sidebarWidth: 260,
key: null,
db: 0,
version: 0,
metaVersion: 0,
pattern: '',
clientHeight: this.$window.height() - $('#tabGroupWrapper').height()
}
}
componentDidMount() {
this.updateLayoutBinded = this.updateLayout.bind(this)
$(window).on('resize', this.updateLayoutBinded)
this.updateLayout()
}
componentWillUnmount() {
$(window).off('resize', this.updateLayoutBinded)
}
updateLayout() {
this.setState({
clientHeight: this.$window.height() - $('#tabGroupWrapper').height()
})
}
handleCreateKey(key) {
this.setState({key, pattern: key})
}
render() {
return ( {
this.setState({sidebarWidth: size})
}}
>
this.setState({pattern})}
height={this.state.clientHeight}
width={this.state.sidebarWidth}
redis={this.props.redis}
connectionKey={this.props.connectionKey}
onSelectKey={key => this.setState({key, version: this.state.version + 1})}
onCreateKey={this.handleCreateKey.bind(this)}
db={this.state.db}
onDatabaseChange={db => this.setState({db})}
onKeyMetaChange={() => this.setState({metaVersion: this.state.metaVersion + 1})}
/>
this.setState({db})}
/>
)
}
}
function mapStateToProps(state, {instance}) {
return {
patterns: state.patterns,
redis: instance.get('redis'),
connectionKey: instance.get('connectionKey')
}
}
export default connect(mapStateToProps)(Database)
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/index.scss
================================================
.Resizer {
background: #000;
opacity: .2;
z-index: 1;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
.Resizer.horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
.Resizer.vertical {
width: 11px;
margin: 0 -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize;
height: 100%;
}
.overflow-wrapper {
display: flex;
width: calc(100% + 16px);
margin-left: -8px;
span {
padding: 0 8px;
display: block;
flex: 1;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
span[contenteditable="true"] {
text-overflow: clip;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/Modal/index.jsx
================================================
import React from 'react'
import ReactDOM from 'react-dom'
require('json-editor')
require('./index.scss')
export default class Modal extends React.Component {
handleSubmit() {
if (this.editor) {
const errors = this.editor.validate()
if (errors.length) {
$('.ui-state-error', ReactDOM.findDOMNode(this.refs.form)).css('opacity', 1)
return
}
this.props.onSubmit(this.editor.getValue())
} else {
this.props.onSubmit(1)
}
}
handleCancel() {
this.props.onCancel()
}
componentDidMount() {
if (this.props.form) {
this.editor = new JSONEditor(ReactDOM.findDOMNode(this.refs.form), {
disable_array_add: true,
disable_array_delete: true,
disable_array_reorder: true,
disable_collapse: true,
disable_edit_json: true,
disable_properties: true,
required_by_default: true,
schema: this.props.form,
show_errors: 'always',
theme: 'jqueryui'
})
$('.row input, .row select', ReactDOM.findDOMNode(this.refs.form)).first().focus()
} else {
$('.nt-button', ReactDOM.findDOMNode(this)).first().focus()
}
}
handleKeyDown(evt) {
if (evt.keyCode === 9) {
const $all = $('.row input, .row select, .nt-button', ReactDOM.findDOMNode(this))
const focused = $(':focus')[0]
let i
for (i = 0; i < $all.length - 1; ++i) {
if ($all[i] != focused) {
continue
}
$all[i + 1].focus()
$($all[i + 1]).select()
break
}
// Must have been focused on the last one or none of them.
if (i == $all.length - 1) {
$all[0].focus()
$($all[0]).select()
}
evt.stopPropagation()
evt.preventDefault()
return
}
if (evt.keyCode === 27) {
this.handleCancel()
evt.stopPropagation()
evt.preventDefault()
return
}
if (evt.keyCode === 13) {
const node = ReactDOM.findDOMNode(this.props.form ? this.refs.cancel : this.refs.submit)
node.focus()
setTimeout(() => {
node.click()
}, 10)
evt.stopPropagation()
evt.preventDefault()
}
}
render() {
return (
{
this.props.title &&
{this.props.title}
}
{!this.props.form &&
}
{this.props.content}
Cancel
{this.props.button || 'OK'}
)
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/Modal/index.scss
================================================
.Modal {
position: fixed;
left: 0;
width: 100%;
z-index: 999;
height: calc(100% + 100px);
margin-top: -100px;
}
.Modal__title {
font-size: 14px;
font-weight: bold;
margin-bottom: 10px;
}
.Modal__content {
position: relative;
width: 420px;
background: #efefef;
border: 1px solid #a3a3a3;
border-top: 0;
padding: 18px 20px 18px 100px;
box-shadow: inset 1px 4px 9px -6px, 0 5px 20px rgba(0, 0, 0, 0.3);
margin: 100px auto 0;
font-size: 12px;
.nt-button-group {
margin-top: 20px;
}
* {
-webkit-user-select: text;
}
}
.Modal__icon {
position: absolute;
left: 20px;
top: 22px;
width: 62px;
height: 57px;
background: transparent url(./warning.png) left top no-repeat;
background-size: 62px 57px;
span {
position: absolute;
bottom: -6px;
right: -6px;
width: 34px;
height: 34px;
background: transparent url(./icon.png) left top no-repeat;
background-size: 34px 34px;
}
}
.Modal__form {
h3 {
display: none;
}
.ui-corner-all {
padding: 0 !important;
margin: 0 !important;
}
.form-control {
position: relative;
background: none;
border: 0;
padding: 4px 0px 14px !important;
label {
position: absolute;
left: -90px;
text-align: right;
width: 80px;
font-weight: normal !important;
}
input, select {
width: 100% !important;
margin: 0 !important;
}
.ui-state-error {
position: absolute;
top: 25px;
opacity: 0;
}
}
.ui-state-error {
color: #ff2a1c;
font-size: 12px;
}
// input, select {
// width: 100%;
// min-height: 25px;
// padding: 5px 10px;
// line-height: 1.6;
// background-color: #fff;
// border: 1px solid #ddd;
// outline: 0;
// &:focus {
// border-radius: 4px;
// border-color: #6db3fd;
// box-shadow: 3px 3px 0 #6db3fd, -3px -3px 0 #6db3fd, -3px 3px 0 #6db3fd, 3px -3px 0 #6db3fd;
// }
// }
}
.modal-enter {
.Modal__content {
transform: translateY(-100%);
}
}
.modal-enter.modal-enter-active {
.Modal__content {
transform: translateY(0);
transition: transform 150ms linear;
}
}
.modal-leave {
.Modal__content {
transform: translateY(0);
}
}
.modal-leave.modal-leave-active {
.Modal__content {
transform: translateY(-100%);
transition: transform 150ms linear;
}
}
================================================
FILE: src/renderer/windows/MainWindow/InstanceContent/index.jsx
================================================
'use strict'
import React, {PureComponent} from 'react'
import ConnectionSelectorContainer from './ConnectionSelectorContainer'
import DatabaseContainer from './DatabaseContainer'
import Modal from './Modal'
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
class InstanceContent extends PureComponent {
constructor() {
super()
this.state = {}
}
componentDidMount() {
window.showModal = modal => {
this.activeElement = document.activeElement
this.setState({modal})
return new Promise((resolve, reject) => {
this.promise = {resolve, reject}
})
}
}
modalSubmit(result) {
this.promise.resolve(result)
this.setState({modal: null})
if (this.activeElement) {
this.activeElement.focus()
}
}
modalCancel() {
this.promise.reject()
this.setState({modal: null})
if (this.activeElement) {
this.activeElement.focus()
}
}
componentWillUnmount() {
delete window.showModal
}
render() {
const {instances, activeInstanceKey} = this.props
const contents = instances.map(instance => (
{
instance.get('redis')
?
:
}
))
return (
{
this.state.modal &&
}
{contents}
)
}
}
export default InstanceContent
================================================
FILE: src/renderer/windows/MainWindow/InstanceTabs/Tab.tsx
================================================
import React, {memo} from 'react'
import {SortableElement} from 'react-sortable-hoc'
interface ITabProps {
instanceKey: string,
title?: string,
active: boolean,
onTabClick: (key: string) => void,
onTabCloseButtonClick: (key: string) => void
}
function Tab({instanceKey, onTabClick, onTabCloseButtonClick, active, title = 'Quick Connect'}: ITabProps) {
return {
onTabClick(instanceKey)
}}
className={active ? 'tab-item active' : 'tab-item'}
>
{title}
onTabCloseButtonClick(instanceKey)}>
}
export default memo(SortableElement(Tab))
================================================
FILE: src/renderer/windows/MainWindow/InstanceTabs/Tabs.tsx
================================================
import React, {memo} from 'react'
import {SortableContainer} from 'react-sortable-hoc'
import Tab from './Tab'
interface ITabsProps {
instances: any
activeInstanceKey: string
onTabSelect: (key: string) => void
onTabClose: (key: string) => void
}
function Tabs({instances, activeInstanceKey, onTabSelect, onTabClose}: ITabsProps) {
return (
{instances.map((instance, index) => {
const key = instance.get('key')
return
})}
)
}
export default memo(SortableContainer(Tabs))
================================================
FILE: src/renderer/windows/MainWindow/InstanceTabs/index.tsx
================================================
import React, {memo} from 'react'
import Tabs from './Tabs'
require('./main.scss')
function isModalShown() {
return $('.Modal').length > 0
}
let display = 'flex'
interface IInstanceTabsProps {
instances: any
activeInstanceKey: string
onCreateInstance: any
onSelectInstance: any
onDelInstance: any
onMoveInstance: any
}
function InstanceTabs({
onCreateInstance, onSelectInstance, onDelInstance, instances, activeInstanceKey, onMoveInstance
}: IInstanceTabsProps) {
const handleAddButtonClick = () => {
if (!isModalShown()) {
onCreateInstance()
}
}
const handleTabSelect = (key: string) => {
if (!isModalShown()) {
onSelectInstance(key)
}
}
const handleTabClose = (key: string) => {
if (!isModalShown()) {
onDelInstance(key)
}
}
const currentDisplay = instances.count() === 1 ? 'none' : 'flex'
if (display !== currentDisplay) {
display = currentDisplay
setTimeout(() => $(window).trigger('resize'), 0)
}
return
{
if (oldIndex !== newIndex) {
onMoveInstance(instances.getIn([oldIndex, 'key']), instances.getIn([newIndex, 'key']))
}
}}
shouldCancelStart={(e) => (e.target as any).nodeName.toUpperCase() === 'SPAN'}
/>
{'+'}
}
export default memo(InstanceTabs)
================================================
FILE: src/renderer/windows/MainWindow/InstanceTabs/main.scss
================================================
.instance-tabs {
* {
-webkit-user-select: none;
}
display: flex;
li {
position: relative;
flex-grow: 1;
background: #bebebe;
color: #424242;
border: 1px solid #a0a0a0;
border-right: none;
text-align: center;
line-height: 22px;
cursor: default;
&:first-child {
border-left: none;
}
&.is-active,
&.is-active:hover {
background: #d3d3d3;
color: #000000;
border-top-color: #d3d3d3;
.rdTabCloseIcon:hover {
background: #c0c0c0;
color: #6c6c6c;
}
}
&:hover {
background: #b2b2b2;
color: #3e3e3e;
.rdTabCloseIcon {
display: block;
}
}
}
}
.instance-tabs__add {
flex-grow: 0 !important;
flex-basis: 24px;
&:hover {
background: #bebebe !important;
color: #424242 !important;
}
}
.rdTabCloseIcon {
position: absolute;
left: 4px;
top: 4px;
display: none;
border-radius: 2px;
line-height: 1em;
width: 14px;
height: 14px;
&:hover {
color: #5b5b5b;
background: #a2a2a2;
}
}
================================================
FILE: src/renderer/windows/MainWindow/entry.jsx
================================================
'use strict'
require('../../photon/css/photon.min.css')
require('../../../../node_modules/fixed-data-table-contextmenu/dist/fixed-data-table.css')
import ReactDOM from 'react-dom'
import MainWindow from './'
import {ipcRenderer} from 'electron'
import store from 'Redux/store'
import * as actions from 'Redux/actions'
require('../../styles/global.scss')
window.$ = window.jQuery = require('jquery');
window.Buffer = global.Buffer;
ipcRenderer.on('action', (evt, action) => {
if ($('.Modal').length && action.indexOf('Instance') !== -1) {
return
}
store.skipPersist = true
store.dispatch(actions[action]())
store.skipPersist = false
})
ReactDOM.render(MainWindow, document.body.appendChild(document.createElement('div')))
================================================
FILE: src/renderer/windows/MainWindow/index.jsx
================================================
'use strict'
import React, {PureComponent} from 'react'
import {createSelector} from 'reselect'
import {Provider, connect} from 'react-redux'
import InstanceTabs from './InstanceTabs'
import InstanceContent from './InstanceContent'
import DocumentTitle from 'react-document-title'
import {createInstance, selectInstance, delInstance, moveInstance} from 'Redux/actions'
import store from 'Redux/store'
class MainWindow extends PureComponent {
componentDidMount() {
$(window).on('keydown.redis', this.onHotKey.bind(this))
}
componentWillUnmount() {
$(window).off('keydown.redis')
}
onHotKey(e) {
const {instances, selectInstance} = this.props
if (!e.ctrlKey && e.metaKey) {
const code = e.keyCode
if (code >= 49 && code <= 57) {
const number = code - 49
if (number === 8) {
const instance = instances.get(instances.count() - 1)
if (instance) {
selectInstance(instance.get('key'))
return false
}
} else {
const instance = instances.get(number)
if (instance) {
selectInstance(instance.get('key'))
return false
}
}
}
}
return true
}
getTitle() {
const {activeInstance} = this.props
if (!activeInstance) {
return ''
}
const version = activeInstance.get('version')
? `(Redis ${activeInstance.get('version')}) `
: ''
return version + activeInstance.get('title')
}
render() {
const {instances, activeInstance, createInstance,
selectInstance, delInstance, moveInstance} = this.props
return (
)
}
}
const selector = createSelector(
state => state.instances,
state => state.activeInstanceKey,
(instances, activeInstanceKey) => {
return {
instances,
activeInstance: instances.find(instance => instance.get('key') === activeInstanceKey)
}
}
)
const mapDispatchToProps = {
createInstance,
selectInstance,
delInstance,
moveInstance
}
const MainWindowContainer = connect(selector, mapDispatchToProps)(MainWindow)
export default
================================================
FILE: src/renderer/windows/PatternManagerWindow/app.scss
================================================
.patternList {
background: #fff;
border: 1px solid #c5c5c5;
width: 210px;
position: absolute;
top: 20px;
left: 20px;
height: 236px;
footer {
position: absolute;
bottom: 0;
left: 0;
width: 208px;
height: 19px;
background: #fafafa;
border-top: 1px solid #b4b4b4;
button {
width: 23px;
height: 18px;
border-radius: 0;
padding: 0;
border-left: 1px solid #b4b4b4;
border-right: 1px solid #b4b4b4;
margin-left: -1px;
border-top: 0;
border-bottom: 0;
background: #fafafa;
&.is-disabled {
color: #bfbfbf;
}
}
}
.nav-group-item:active {
background-color: #116cd6 !important;
color: #fff;
}
}
.nav-group-item {
padding: 0 5px;
&.sortable-chosen {
color: #000;
&.is-active {
background: #116cd6 !important;
color: #fff;
}
}
&.is-active {
background: #116cd6;
color: #fff;
}
}
.form {
position: absolute !important;
right: 20px;
top: 20px;
width: 328px;
height: 236px;
}
================================================
FILE: src/renderer/windows/PatternManagerWindow/entry.jsx
================================================
'use strict'
require('../../photon/css/photon.min.css')
import React from 'react'
import ReactDOM from 'react-dom'
import {Provider} from 'react-redux'
import PatternManagerWindow from './'
import store from 'Redux/store'
import * as actions from 'Redux/actions'
import {remote, ipcRenderer} from 'electron'
require('../../styles/global.scss')
window.$ = window.jQuery = require('jquery');
ipcRenderer.on('action', (evt, action) => {
if (type === 'delInstance') {
remote.getCurrentWindow().close()
return
}
store.skipPersist = true
store.dispatch(actions[action]())
store.skipPersist = false
})
ReactDOM.render(
,
document.body.appendChild(document.createElement('div'))
)
================================================
FILE: src/renderer/windows/PatternManagerWindow/index.jsx
================================================
import React from 'react'
import {connect} from 'react-redux'
import {createPattern, updatePattern, removePattern} from 'Redux/actions'
import {List} from 'immutable'
require('./app.scss')
const connectionKey = getParameterByName('arg')
class App extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {index: 0}
}
handleChange(property, e) {
this.setState({[property]: e.target.value})
}
select(index) {
this.setState({
index,
name: null,
value: null
})
}
renderPatternForm(activePattern) {
if (!activePattern) {
return null
}
return
}
render() {
const {patterns, createPattern, removePattern} = this.props
const activePattern = patterns.get(this.state.index)
return (
{
const index = patterns.size
createPattern(connectionKey)
this.select(index)
}}
>+
{
if (activePattern) {
removePattern(connectionKey, this.state.index)
this.select(this.state.index > 0 ? this.state.index - 1 : 0)
}
}}
>-
{this.renderPatternForm(activePattern)}
)
}
}
function mapStateToProps(state) {
return {
patterns: state.patterns.get(connectionKey, List())
}
}
const mapDispatchToProps = {
updatePattern,
createPattern,
removePattern
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
function getParameterByName(name) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
const regex = new RegExp('[\\?&]' + name + '=([^]*)')
const results = regex.exec(location.search)
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '))
}
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"allowJs": true,
"target": "es6",
"lib": [
"es6"
],
"moduleResolution": "node",
"types": [
"node"
],
"typeRoots": [
"node_modules/@types"
],
"module": "commonjs",
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": [
"./src/**/*"
]
}
================================================
FILE: webpack.config.js
================================================
'use strict';
const {resolve} = require('path')
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const {CheckerPlugin} = require('awesome-typescript-loader')
const webpack = require('webpack')
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'
const watch = process.env.WEBPACK_WATCH === 'true'
const distPath = resolve(__dirname, 'dist')
const base = {
mode, watch,
output: {
path: distPath,
chunkFilename: '[name].chunk.js',
filename: '[name].js'
},
node: {
Buffer: false,
buffer: false,
__dirname: false,
__filename: false,
},
module: {
rules: [{
test: /\.(ts|tsx)$/,
use: [{
loader: 'awesome-typescript-loader',
options: {
reportFiles: ['src/**/*.{ts,tsx}'],
useCache: true,
useBabel: true,
babelCore: '@babel/core'
}
}],
exclude: /node_modules/
}, {
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader']
}, {
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader'
]
}, {
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}, {
test: /\.(png|jpg)$/,
use: [{
loader: "file-loader"
}]
}, {
test: /\.(eot|woff|ttf)$/,
use: [{
loader: "file-loader"
}]
}]
},
externals: {
'system': '{}', // jsonlint
'file': '{}' // jsonlint
},
}
const renderPlugins = [
new HtmlWebpackPlugin({title: 'Medis', chunks: ['main'], filename: 'main.html'}),
new HtmlWebpackPlugin({title: 'Manage Patterns', chunks: ['patternManager'], filename: 'patternManager.html'}),
new MiniCssExtractPlugin({filename: '[name].css'}),
new CheckerPlugin(),
new webpack.ProvidePlugin({React: 'react'}),
]
if (mode === 'production') {
renderPlugins.push(new BundleAnalyzerPlugin())
}
const renderer = Object.assign({}, base, {
target: 'electron-renderer',
output: Object.assign({}, base.output, {
path: resolve(base.output.path, 'renderer')
}),
entry: {
main: resolve(__dirname, 'src/renderer/windows/MainWindow/entry.jsx'),
patternManager: resolve(__dirname, 'src/renderer/windows/PatternManagerWindow/entry.jsx')
},
plugins: renderPlugins,
resolve: {
alias: {
Redux: resolve(__dirname, 'src/renderer/redux/'),
Utils: resolve(__dirname, 'src/renderer/utils/'),
},
extensions: ['.js', '.jsx', '.ts', '.tsx']
}
})
const main = Object.assign({}, base, {
target: 'electron-main',
output: Object.assign({}, base.output, {
path: resolve(base.output.path, 'main')
}),
entry: {
index: resolve(__dirname, 'src/main/index.ts')
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx']
}
})
module.exports = [main, renderer]