Full Code of luin/medis for AI

master 12c87a5a2cc3 cached
92 files
214.7 KB
62.9k tokens
271 symbols
1 requests
Download .txt
Showing preview only (239K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<a name="0.6.1"></a>
## [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)



<a name="0.6.0"></a>
# [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)



<a name="0.5.0"></a>
# [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)



<a name="0.5.0"></a>
# [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)



<a name="0.3.0"></a>
# [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)



<a name="0.2.1"></a>
## [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!

[![Download on the App Store](http://getmedis.com/download.svg)](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](http://getmedis.com/screen.png)

---

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).

[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](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.

<hr />

## 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. <br> [![Download on the App Store](http://getmedis.com/download.svg)](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

<table><tr><td width="20%"><a href="https://github.com/luin"><img src="https://avatars1.githubusercontent.com/u/635902?v=3" /></a><p align="center">luin</p></td><td width="20%"><a href="https://github.com/kvnsmth"><img src="https://avatars0.githubusercontent.com/u/127?v=3" /></a><p align="center">kvnsmth</p></td><td width="20%"><a href="https://github.com/dpde"><img src="https://avatars2.githubusercontent.com/u/485645?v=3" /></a><p align="center">dpde</p></td><td width="20%"><a href="https://github.com/ogasawaraShinnosuke"><img src="https://avatars1.githubusercontent.com/u/5368888?v=3" /></a><p align="center">ogasawaraShinnosuke</p></td><td width="20%"><a href="https://github.com/naholyr"><img src="https://avatars1.githubusercontent.com/u/214067?v=3" /></a><p align="center">naholyr</p></td></tr><tr><td width="20%"><a href="https://github.com/hlobil"><img src="https://avatars2.githubusercontent.com/u/484499?v=3" /></a><p align="center">hlobil</p></td><td width="20%"><a href="https://github.com/Janpot"><img src="https://avatars1.githubusercontent.com/u/2109932?v=3" /></a><p align="center">Janpot</p></td></table>

## 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 <i@zihua.li> (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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.inherit</key>
    <true/>
  </dict>
</plist>


================================================
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-only</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
  </dict>
</plist>


================================================
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<BrowserWindow>()

  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 <http://jcubic.pl>
 *
 */
.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 (<div className="nt-form-row">
      <label htmlFor="cert">{label}:</label>
      <input
        type="text"
        id={id}
        readOnly
        value={this.getProp(`${id}File`)}
        placeholder={`Select ${label} File (PEM)`}
      />
      <button
        className={'icon icon-dot-3 ssh-key'}
        onClick={() => {
          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})
          }
        }}
      />
    </div>)
  }

  render() {
    return (<div>
      <div className="nt-box" style={{width: 500, margin: '60px auto 0'}}>
        <div className="nt-form-row" style={{display: this.props.favorite ? 'block' : 'none'}}>
          <label htmlFor="name">Name:</label>
          <input type="text" id="name" value={this.getProp('name')} onChange={this.handleChange.bind(this, 'name')} placeholder="Bookmark name" />
        </div>
        <div className="nt-form-row">
          <label htmlFor="host">Redis Host:</label>
          <input type="text" id="host" value={this.getProp('host')} onChange={this.handleChange.bind(this, 'host')} placeholder="localhost" />
        </div>
        <div className="nt-form-row">
          <label htmlFor="port">Port:</label>
          <input type="number" id="port" value={this.getProp('port')} onChange={this.handleChange.bind(this, 'port')} placeholder="6379" min="0" max="65535"/>
        </div>
        <div className="nt-form-row">
          <label htmlFor="password">Password:</label>
          <input type="password" id="password" onChange={this.handleChange.bind(this, 'password')} value={this.getProp('password')} />
        </div>
        <div className="nt-form-row">
          <label htmlFor="ssh">SSL:</label>
          <input type="checkbox" id="ssl" onChange={this.handleChange.bind(this, 'ssl')} checked={this.getProp('ssl')} />
        </div>
        <div style={{display: this.getProp('ssl') ? 'block' : 'none'}}>
          {this.renderCertInput('Private Key', 'tlskey')}
          {this.renderCertInput('Certificate', 'tlscert')}
          {this.renderCertInput('CA', 'tlsca')}
        </div>
        <div className="nt-form-row">
          <label htmlFor="ssh">SSH Tunnel:</label>
          <input type="checkbox" id="ssh" onChange={this.handleChange.bind(this, 'ssh')} checked={this.getProp('ssh')} />
        </div>
        <div style={{display: this.getProp('ssh') ? 'block' : 'none'}}>
          <div className="nt-form-row">
            <label htmlFor="sshHost">SSH Host:</label>
            <input type="text" id="sshHost" onChange={this.handleChange.bind(this, 'sshHost')} value={this.getProp('sshHost')} placeholder="" />
          </div>
          <div className="nt-form-row">
            <label htmlFor="sshUser">SSH User:</label>
            <input type="text" id="sshUser" onChange={this.handleChange.bind(this, 'sshUser')} value={this.getProp('sshUser')} placeholder="" />
          </div>
          <div className="nt-form-row">
            <label htmlFor="sshPassword">SSH {this.getProp('sshKey') ? 'Key' : 'Password'}:</label>
            <input
              type={this.getProp('sshKeyFile') ? 'text' : 'password'}
              id="sshPassword"
              readOnly={Boolean(this.getProp('sshKey'))}
              onChange={this.handleChange.bind(this, 'sshPassword')}
              value={this.getProp('sshKeyFile') || this.getProp('sshPassword')}
              placeholder=""
            />
            <button
              className={'icon icon-key ssh-key' + (this.getProp('sshKey') ? ' is-active' : '')}
              onClick={() => {
                if (this.getProp('sshKey')) {
                  this.setProp({
                    sshKey: false,
                    sshKeyFile: false
                  })
                  return
                }
                const win = remote.getCurrentWindow()
                const files = remote.dialog.showOpenDialog(win, {
                  message: 'Select a private key (Most often in the ~/.ssh)',
                  properties: ['openFile', 'showHiddenFiles']
                })
                if (files && files.length) {
                  const file = files[0]
                  const content = fs.readFileSync(file, 'utf8')
                  this.setProp({sshKey: content, sshKeyFile: file})
                }
              }}
            />
          </div>
          <div className="nt-form-row" style={{display: this.getProp('sshKey') && this.getProp('sshKey').indexOf('ENCRYPTED') > -1 ? 'block' : 'none'}}>
            <label htmlFor="sshKeyPassphrase">SSH Key Passphrase:</label>
            <input type="password" id="sshKeyPassphrase" onChange={this.handleChange.bind(this, 'sshKeyPassphrase')} value={this.getProp('sshKeyPassphrase')} />
          </div>
          <div className="nt-form-row">
            <label htmlFor="sshPort">SSH Port:</label>
            <input type="number" min="0" max="65535" id="sshPort" placeholder="22" onChange={this.handleChange.bind(this, 'sshPort')} value={this.getProp('sshPort')} />
          </div>
        </div>
      </div>
      <div className="nt-button-group nt-button-group--pull-right" style={{width: 500, margin: '10px auto 0', paddingBottom: 10}}>
        <button
          className="nt-button" style={{float: 'left'}} onClick={() => {
            this.duplicate()
          }}
        >{this.props.favorite ? 'Duplicate' : 'Add to Favorite'}</button>
        <button
          className="nt-button"
          style={{display: this.state.changed ? 'inline-block' : 'none'}}
          onClick={() => {
            this.save()
          }}
        >Save Changes</button>
        <button
          disabled={Boolean(this.props.connectStatus)} ref="connectButton" className="nt-button nt-button--primary" onClick={() => {
            this.connect()
          }}
        >{this.props.connectStatus || (this.state.changed ? 'Save and Connect' : 'Connect')}</button>
      </div>
    </div>)
  }
}

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 (<div style={{flex: 1, display: 'flex', flexDirection: 'column', overflowY: 'hidden'}}>
      <nav className="nav-group">
        <h5 className="nav-group-title"/>
        <a
          className={'nav-group-item' + (this.state.activeKey ? '' : ' active')}
          onClick={this.onClick.bind(this, -1)}
          onDoubleClick={this.onDoubleClick.bind(this, -1)}
          >
          <span className="icon icon-flash"/>
          QUICK CONNECT
        </a>
        <h5 className="nav-group-title">FAVORITES</h5>
        <div ref="sortable" key={this.sortableKey}>{
          this.props.favorites.map((favorite, index) => {
            return (<a
              key={favorite.get('key')}
              className={'nav-group-item' + (favorite.get('key') === this.state.activeKey ? ' active' : '')}
              onClick={this.onClick.bind(this, index)}
              onDoubleClick={this.onDoubleClick.bind(this, index)}
              >
              <span className="icon icon-home"/>
              <span>{favorite.get('name')}</span>
            </a>)
          })
        }</div>
      </nav>
      <footer className="toolbar toolbar-footer">
        <button
          onClick={() => {
            this.props.createFavorite()
          // TODO: auto select
          // this.select(favorite);
          }}
          >+</button>
        <button
          onClick={
          () => {
            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)
            })
          }
        }
          >-</button>
      </footer>
    </div>)
  }

  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 (<div className="pane-group">
      <aside className="pane pane-sm sidebar">
        <Favorite
          favorites={this.props.favorites}
          onSelect={this.handleSelectFavorite.bind(this, false)}
          onRequireConnecting={this.handleSelectFavorite.bind(this, true)}
          updateFavorite={this.props.updateFavorite}
          createFavorite={this.props.createFavorite}
          removeFavorite={this.props.removeFavorite}
          reorderFavorites={this.props.reorderFavorites}
          />
      </aside>
      <div className="pane">
        <Config
          favorite={selectedFavorite}
          connectStatus={this.props.connectStatus}
          connect={this.state.connect}
          connectToRedis={this.props.connectToRedis}
          onSave={data => {
            this.props.updateFavorite(selectedFavorite.get('key'), data)
          }}
          onDuplicate={this.props.createFavorite}
          />
      </div>
    </div>)
  }
}

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 (<div className="AddButton">
    {title}
    {reload && <span className="reload icon icon-cw" onClick={onReload} />}
    <span className="plus" onClick={onClick}>+</span>
  </div>)
}

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 (<div
      key={group.name}
      className="config-group"
      >
      <h3>{group.name}</h3>
      { group.configs.map(this.renderConfig, this) }
    </div>)
  }

  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 = (<input
        type="checkbox" checked={config.value === 'yes'} onChange={e => {
          config.value = e.target.checked ? 'yes' : 'no'
          this.change(config)
        }} {...props}
           />)
    } else if (config.type === 'number' && String(parseInt(config.value, 10)) === config.value) {
      input = (<input
        type="number" value={config.value} onChange={e => {
          config.value = e.target.value
          this.change(config)
        }} {...props}
           />)
    } else if (Array.isArray(config.type) && config.type.indexOf(config.value) !== -1) {
      input = (<select
        value={config.value} onChange={e => {
          config.value = e.target.value
          this.change(config)
        }} {...props}
           >
        {config.type.map(option => <option key={option}>{option}</option>)}
      </select>)
    } else {
      input = (<input
        type="text" value={config.value} onChange={e => {
          config.value = e.target.value
          this.change(config)
        }} {...props}
           />)
    }
    return (<div
      className="nt-form-row"
      key={config.name}
      >
      <label htmlFor={config.name}>{config.name}</label>
      { input }
      <div className="description">{config.description}</div>
    </div>)
  }

  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 (<div style={this.props.style} className="Config">
      <div className="wrapper">
        <form>
          {
            this.state.groups.map(this.renderGroup, this)
          }
        </form>
        <div className="nt-button-group nt-button-group--pull-right">
          <button
            ref="submit"
            className="nt-button"
            onClick={this.handleReload.bind(this)}
            >Reload</button>
          <button
            ref="submit"
            className="nt-button"
            disabled={!this.isChanged(true)}
            onClick={this.handleSave.bind(this)}
            >Save To Config File</button>
          <button
            ref="cancel"
            className="nt-button nt-button--primary"
            disabled={!this.isChanged()}
            onClick={() => {
              this.handleApply()
            }}
            >Apply</button>
        </div>
      </div>
    </div>)
  }
}

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 (<footer className="toolbar toolbar-footer">
      {
        desc.map(({key, value}) => <span
          key={key}
          style={{margin: '0 5px'}}
        >{value}</span>)
      }
    </footer>)
  }
}

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 = (<Codemirror
        ref="codemirror"
        key="raw"
        value={this.state.modes.raw}
        onChange={this.updateContent.bind(this, 'raw')}
        options={{
          mode: 'none',
          styleActiveLine: true,
          lineWrapping: this.state.wrapping,
          gutters: ['CodeMirror-lint-markers'],
          lineNumbers: true
        }}
        />)
    } else if (this.state.currentMode === 'json') {
      viewer = (<Codemirror
        ref="codemirror"
        key="json"
        value={this.state.modes.json}
        onChange={this.updateContent.bind(this, 'json')}
        options={{
          mode: {
            name: 'javascript',
            json: true
          },
          tabSize: 2,
          indentWithTabs: true,
          styleActiveLine: true,
          lineNumbers: true,
          lineWrapping: this.state.wrapping,
          gutters: ['CodeMirror-lint-markers'],
          autoCloseBrackets: true,
          matchTags: true,
          lint: Boolean(this.state.modes.raw)
        }}
        />)
    } else if (this.state.currentMode === 'messagepack') {
      viewer = (<Codemirror
        ref="codemirror"
        key="messagepack"
        value={this.state.modes.messagepack}
        onChange={this.updateContent.bind(this, 'messagepack')}
        options={{
          mode: {
            name: 'javascript',
            json: true
          },
          tabSize: 2,
          indentWithTabs: true,
          styleActiveLine: true,
          lineNumbers: true,
          lineWrapping: this.state.wrapping,
          gutters: ['CodeMirror-lint-markers'],
          autoCloseBrackets: true,
          matchTags: true,
          lint: Boolean(this.state.modes.raw)
        }}
        />)
    } else {
      viewer = <div/>
    }
    return (<div
      style={{flex: 1, display: 'flex', flexDirection: 'column'}}
      className="Editor"
      onKeyDown={this.handleKeyDown.bind(this)}
      >
      { viewer }
      <div
        className="operation-pannel"
        >
        <label className="wrap-selector" ref="wrapSelector">
          <input
            type="checkbox"
            checked={this.state.wrapping}
            onChange={evt => this.setState({wrapping: evt.target.checked})}
            />
          <span>Wrapping</span>
        </label>
        <select
          className="mode-selector"
          value={this.state.currentMode}
          onChange={this.updateMode.bind(this)}
          >
          <option value="raw" disabled={typeof this.state.modes.raw !== 'string'}>Raw</option>
          <option value="json" disabled={typeof this.state.modes.json !== 'string'}>JSON</option>
          <option value="messagepack" disabled={typeof this.state.modes.messagepack !== 'string'}>MessagePack</option>
        </select>
        <button
          className="nt-button"
          disabled={!this.state.changed}
          onClick={this.save.bind(this)}
          >Save Changes</button>
      </div>
    </div>)
  }
}

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 (<SplitPane
        minSize={80}
        split="vertical"
        ref="node"
        defaultSize={this.props.contentBarWidth}
        onChange={this.props.setSize.bind(null, 'content')}
      >
      <div
        style={{marginTop: -1}}
        onKeyDown={this.handleKeyDown.bind(this)}
        tabIndex="0"
        ref="table"
        className={'base-content ' + this.randomClass}
        >
        <Table
          rowHeight={24}
          rowsCount={this.state.length}
          rowClassNameGetter={this.rowClassGetter.bind(this)}
          onRowContextMenu={this.showContextMenu.bind(this)}
          onRowClick={this.handleSelect.bind(this)}
          onRowDoubleClick={(evt, index) => {
            this.handleSelect(evt, index)
            this.setState({editableIndex: index})
          }}
          width={this.props.contentBarWidth}
          height={this.props.height}
          headerHeight={24}
          >
          <Column
            header={
              <AddButton
                title="key" onClick={() => {
                  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 (<ContentEditable
                className="ContentEditable overflow-wrapper"
                enabled={rowIndex === this.state.editableIndex}
                onChange={target => {
                  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]}
                />)
            }}
            />
        </Table>
      </div>
      <Editor
        buffer={this.state.content}
        onSave={this.save.bind(this)}
        />
    </SplitPane>)
  }
}

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 <Editor
      buffer={buffer}
      onSave={this.save.bind(this)}
      />
  }

  renderIndexColumn() {
    return <Column
      header={
        <SortHeaderCell
          title="index"
          onOrderChange={desc => this.setState({
            desc,
            members: [],
            selectedIndex: null
          })}
          desc={this.state.desc}
          />
      }
      width={this.props.indexBarWidth}
      isResizable
      cell={({rowIndex}) => {
        return <div className="index-label">{ this.state.desc ? this.state.length - 1 - rowIndex : rowIndex }</div>
      }}
      />
  }

  renderValueColumn() {
    return <Column
      header={
        <AddButton
          title="item" onClick={async () => {
            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 <div className="overflow-wrapper"><span>{data}</span></div>
      }}
      />
  }

  render() {
    return (<SplitPane
        minSize={80}
        split="vertical"
        ref="node"
        defaultSize={this.props.contentBarWidth}
        onChange={this.props.setSize.bind(null, 'content')}
      >
      <div
        tabIndex="0"
        ref="table"
        onKeyDown={this.handleKeyDown.bind(this)}
        className={'base-content ' + this.randomClass}
        >
        <Table
          rowHeight={24}
          rowsCount={this.state.length}
          rowClassNameGetter={this.rowClassGetter.bind(this)}
          onRowClick={this.handleSelect.bind(this)}
          onRowContextMenu={this.showContextMenu.bind(this)}
          isColumnResizing={false}
          onColumnResizeEndCallback={this.props.setSize.bind(null, 'index')}
          width={this.props.contentBarWidth}
          height={this.props.height}
          headerHeight={24}
          >
          {this.renderIndexColumn()}
          {this.renderValueColumn()}
        </Table>
      </div>
      {this.renderEditor()}
    </SplitPane>)
  }
}

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 (<SplitPane
      className="pane-group"
      minSize={80}
      split="vertical"
      ref="node"
      defaultSize={this.props.contentBarWidth}
      onChange={this.props.setSize.bind(null, 'content')}
      >
      <div
        onKeyDown={this.handleKeyDown.bind(this)}
        tabIndex="0"
        ref="table"
        className={'base-content ' + this.randomClass}
        >
        <Table
          rowHeight={24}
          rowsCount={this.state.length}
          rowClassNameGetter={this.rowClassGetter.bind(this)}
          onRowContextMenu={this.showContextMenu.bind(this)}
          onRowClick={this.handleSelect.bind(this)}
          width={this.props.contentBarWidth}
          height={this.props.height}
          headerHeight={24}
          >
          <Column
            width={this.props.contentBarWidth}
            cell={({rowIndex}) => {
              const member = this.state.members[rowIndex]
              if (typeof member === 'undefined') {
                this.load(rowIndex)
                return 'Loading...'
              }
              return <div className="overflow-wrapper"><span>{member}</span></div>
            }}
            header={
              <AddButton
                title="member" onClick={() => {
                  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)
                      })
                    })
                  })
                }}
                               />
            }
          />
        </Table>
      </div>
      <Editor
        buffer={typeof this.state.content === 'string' && Buffer.from(this.state.content)}
        onSave={this.save.bind(this)}
        />
    </SplitPane>)
  }
}

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 (<Cell
    onClick={handleOnClick}
    >
    <a className={'SortHeaderCell' + (desc ? '' : ' is-asc')}>
      {title}
      {
        <img
          width="7"
          height="4"
          src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAJBAMAAADwYwBaAAAAKlBMVEUAAACfn5/t7e2lpaXAwMD9/f3x8fHGxsapqan4+Pj19fWrq6uhoaG+vr4IBCNyAAAAAXRSTlMAQObYZgAAAERJREFUCNdjmMUAAisZDl4FUiEyDBtFAhhYHaUZMgQdGFgE2xjYGsUZCiUSGBiUBcsFjYBqmAwFhRVAepRBXJCAMZALALm5CbsZPOUxAAAAAElFTkSuQmCC"
        />
      }
    </a>
  </Cell>)
}

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 (<Editor
      buffer={this.state.buffer}
      onSave={this.save.bind(this)}
      />)
  }
}

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 <Table
      rowHeight={24}
      rowsCount={this.state.length}
      rowClassNameGetter={this.rowClassGetter.bind(this)}
      onRowClick={this.handleSelect.bind(this)}
      onRowContextMenu={this.showContextMenu.bind(this)}
      onRowDoubleClick={(evt, index) => {
        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()}
    </Table>
  }

  renderScoreColumn() {
    return <Column
    header={
      <SortHeaderCell
        title="score"
        onOrderChange={desc => 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 (<ContentEditable
        className="ContentEditable overflow-wrapper"
        enabled={rowIndex === this.state.editableIndex}
        onChange={async (newScore) => {
          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 <Column
    header={
      <AddButton
        title="member" onClick={async () => {
          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 <div className="overflow-wrapper"><span>{member[0]}</span></div>
    }}
    />
  }

  renderEditor() {
    const item = this.state.members[this.state.selectedIndex]
    const buffer = item
      ? Buffer.from(item[0])
      : undefined
    return <Editor
      buffer={buffer}
      onSave={this.save.bind(this)}
      />
  }

  render() {
    return (<SplitPane
        minSize={80}
        split="vertical"
        ref="node"
        defaultSize={this.props.contentBarWidth}
        onChange={this.props.setSize.bind(null, 'content')}
      >
      <div
        onKeyDown={this.handleKeyDown.bind(this)}
        tabIndex="0"
        ref="table"
        className={'base-content ' + this.randomClass}
        >
        {this.renderTable()}
      </div>
      {this.renderEditor()}
    </SplitPane>)
  }
}

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 = <StringContent {...props}/>; break
    case 'list': view = <ListContent {...props}/>; break
    case 'set': view = <SetContent {...props}/>; break
    case 'hash': view = <HashContent {...props}/>; break
    case 'zset': view = <ZSetContent {...props}/>; break
    case 'none':
      view = (<div className="notfound">
        <span className="icon icon-trash"/>
        <p>The key has been deleted</p>
      </div>)
      break
    }
    return <div style={this.props.style} className="BaseContent">{ view }</div>
  }
}

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 <span className="icon icon-book" />
    case 'Terminal':
      return <span className="icon icon-window" />
    case 'Config':
      return <span className="icon icon-cog" />
  }
}

function renderTab(tab, {activeTab, onSelectTab}) {
  return <div
    className={'item' + (tab === activeTab ? ' is-active' : '')}
    key={tab}
    onClick={() => onSelectTab(tab)}
  >
    {renderTabIcon(tab)}
    {tab}
  </div>
}

function Content(props) {
  return <div className="TabBar">{TABS.map(tab => renderTab(tab, props))}</div>
}

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 (<div ref="terminal" style={this.props.style} className="Terminal"/>)
  }
}

export default Terminal

function getHTML(response) {
  if (Array.isArray(response)) {
    return `<ul class="array-resp">
    ${response.map((item, index) => '<li><span>' + index + '</span>' + getHTML(item) + '</li>').join('')}
    </ul>`
  }
  const type = typeof response
  if (type === 'number') {
    return `<div class="number">${response}</div>`
  }
  if (type === 'string') {
    return `<div class="string">${response.replace(/\r?\n/g, '<br>')}</div>`
  }
  if (response === null) {
    return `<div class="null">null</div>`
  }
  if (response instanceof Error) {
    return `<div class="error">${response.message}</div>`
  }
  if (type === 'object') {
    return `<ul class="object-resp">
    ${Object.keys(response).map(item => '<li><span class="key">' + item + '</span>' + getHTML(response[item]) + '</li>').join('')}
    <ul>`
  }

  return `<div class="json">${JSON.stringify(response)}</div>`
}

function formatMonitor(time, args) {
  args = args || []
  const command = args[0] ? args.shift().toUpperCase() : ''
  if (command) {
    commands.getKeyIndexes(command.toLowerCase(), args).forEach(index => {
      args[index] = `<span class="command-key">${args[index]}</span>`
    })
  }
  return `<div class="monitor">
    <span class="time">${time}</span>
    <span class="command">
      <span class="command-name">${command}</span>
      <span class="command args">${args.join(' ')}</span>
    </span>
  </div>`
}

function formatMessage(channel, message) {
  return `<div class="monitor">
    <span class="time">${channel}</span>
    <span class="message">${message}</span>
  </div>`
}


================================================
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 (<div className="pane sidebar" style={{height: '100%'}}>
      <TabBar
        activeTab={this.state.tab}
        onSelectTab={this.handleTabChange.bind(this)}
        />
      <KeyContent
        style={{display: this.state.tab === 'Content' ? 'flex' : 'none'}}
        keyName={this.props.keyName}
        keyType={this.state.keyType}
        height={this.props.height - 66}
        redis={this.props.redis}
        onKeyContentChange={() => {
          this.setState({version: this.state.version + 1})
        }}
        />
      <Terminal
        style={{display: this.state.tab === 'Terminal' ? 'block' : 'none'}}
        height={this.props.height - 67}
        redis={this.props.redis}
        connectionKey={this.props.connectionKey}
        onDatabaseChange={this.props.onDatabaseChange}
        />
      <Config
        style={{display: this.state.tab === 'Config' ? 'block' : 'none'}}
        height={this.props.height - 67}
        redis={this.props.redis}
        connectionKey={this.props.connectionKey}
        />
      <Footer
        keyName={this.props.keyName}
        keyType={this.state.keyType}
        version={this.state.version}
        redis={this.props.redis}
        />
    </div>)
  }
}

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 (<div
      {...props}
      >
      <span
        onInput={this.handleChange.bind(this)}
        onKeyDown={this.handleKeyDown.bind(this)}
        onBlur={this.handleSubmit.bind(this)}
        contentEditable={enabled}
        ref="text"
        dangerouslySetInnerHTML={{__html: escape(html)}}
        />
    </div>)
  }

  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 (<footer className="toolbar toolbar-footer">
      <span style={{marginLeft: 6}}>Keys: {keys}</span>
      <div style={{float: 'right'}}>
        <span>DB:</span>
        <select
          onChange={this.handleChange.bind(this)}
          value={this.props.db} className="form-control" style={{
            width: 50,
            marginTop: 2,
            marginRight: 2,
            marginLeft: 3,
            fontSize: 10,
            float: 'right'
          }}
        >
          {(max => {
            return new Array(max).fill(0).map((value, db) => {
              return (
                <option key={db} value={db}>
                  {db} {this.keyCountByDb(db) > 0 ? `(${this.keyCountByDb(db)})` : ''}
                </option>
              );
            });
          })(this.state.databases || 1)}
        </select>
      </div>
    </footer>)
  }
}

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 (<div
      tabIndex="0"
      className={'pattern-table ' + this.randomClass}
      >
      <Table
        rowHeight={24}
        rowsCount={this.state.keys.length + (this.state.cursor === '0' ? 0 : 1)}
        onScrollStart={() => {
          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}
        >
        <Column
          header="type"
          width={40}
          cell={({rowIndex}) => {
            const item = this.state.keys[rowIndex]
            if (!item) {
              return ''
            }
            const cellData = item[1]
            if (!cellData) {
              return ''
            }
            const type = cellData === 'string' ? 'str' : cellData
            return <span className={`key-type ${type}`}>{type}</span>
          }}
          />
        <Column
          header={
            <AddButton
              reload="true" title="name" onReload={() => {
                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 <span style={{color: '#ccc'}}>Scanning...(cursor {this.state.cursor})</span>
              }
              return (<a
                href="#" style={{color: '#666'}} onClick={evt => {
                  evt.preventDefault()
                  this.scan()
                }}>Scan more</a>)
            }
            return (<ContentEditable
              className="ContentEditable overflow-wrapper"
              enabled={cellData === this.state.editableKey}
              onChange={newKeyName => {
                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}
              />)
          }}
          />
      </Table>
    </div>)
  }
}

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 (<div className="pattern-input">
      <span className="icon icon-search"/>
      <input
        type="search"
        className="form-control"
        placeholder="Key name or patterns (e.g. user:*)"
        value={this.state.pattern}
        onChange={evt => {
          this.updatePattern(evt.targ
Download .txt
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
Download .txt
SYMBOL INDEX (271 symbols across 44 files)

FILE: src/main/menu.ts
  method click (line 9) | click() {
  method click (line 15) | click() {
  method click (line 23) | click() {
  method click (line 29) | click() {
  method click (line 67) | click(item, focusedWindow) {
  method click (line 80) | click(item, focusedWindow) {
  method click (line 93) | click(item, focusedWindow) {
  method click (line 116) | click() {
  method click (line 121) | click() {
  method click (line 159) | click() {

FILE: src/main/windowManager.ts
  class WindowManager (line 5) | class WindowManager extends EventEmitter {
    method constructor (line 8) | constructor() {
    method current (line 14) | get current() {
    method create (line 18) | create(type = 'main', arg?: any): BrowserWindow {
    method _register (line 56) | _register(win: BrowserWindow): void {
    method dispatch (line 67) | dispatch(action: string, args: any) {

FILE: src/renderer/redux/actions/connection.js
  function getIndex (line 8) | function getIndex(getState) {
  function handleRedis (line 71) | function handleRedis(config, override) {

FILE: src/renderer/redux/middlewares/createThunkReplyMiddleware.js
  function isThunkReply (line 1) | function isThunkReply(action) {
  function createThunkReplyMiddleware (line 5) | function createThunkReplyMiddleware(extraArgument) {

FILE: src/renderer/redux/reducers/activeInstanceKey.js
  method [createInstance] (line 12) | [createInstance](state, data) {
  method [selectInstance] (line 15) | [selectInstance](state, data) {
  method [moveInstance] (line 18) | [moveInstance](state, {activeInstanceKey}) {
  method [delInstance] (line 21) | [delInstance](state, {activeInstanceKey}) {

FILE: src/renderer/redux/reducers/favorites.js
  function FavoriteFactory (line 12) | function FavoriteFactory(data) {
  method [createFavorite] (line 17) | [createFavorite](state, data) {
  method [removeFavorite] (line 20) | [removeFavorite](state, key) {
  method [updateFavorite] (line 23) | [updateFavorite](state, {key, data}) {
  method [reorderFavorites] (line 26) | [reorderFavorites](state, {from, to}) {
  method [reloadFavorites] (line 30) | [reloadFavorites](state, data) {

FILE: src/renderer/redux/reducers/instances.js
  function InstanceFactory (line 13) | function InstanceFactory({key, data}) {
  method [createInstance] (line 18) | [createInstance](state, data) {
  method [moveInstance] (line 21) | [moveInstance](state, {fromIndex, toIndex}) {
  method [delInstance] (line 25) | [delInstance](state, {targetIndex}) {
  method [updateConnectStatus] (line 28) | [updateConnectStatus](state, {index, status}) {
  method [disconnect] (line 31) | [disconnect](state, {index}) {
  method [connectToRedis] (line 40) | [connectToRedis](state, {index, config, redis}) {

FILE: src/renderer/redux/reducers/patterns.js
  function PatternFactory (line 11) | function PatternFactory(data) {
  method [createPattern] (line 16) | [createPattern](state, {conn, key}) {
  method [removePattern] (line 19) | [removePattern](state, {conn, index}) {
  method [updatePattern] (line 22) | [updatePattern](state, {conn, index, data}) {
  method [reloadPatterns] (line 31) | [reloadPatterns](state, data) {

FILE: src/renderer/redux/reducers/sizes.js
  method [setSize] (line 9) | [setSize](state, {type, value}) {

FILE: src/renderer/storage/Favorites.js
  function get (line 5) | function get() {
  function set (line 10) | function set(favorites) {

FILE: src/renderer/storage/Patterns.js
  function get (line 5) | function get() {
  function set (line 10) | function set(patterns) {

FILE: src/renderer/storage/Sizes.js
  function get (line 3) | function get() {
  function set (line 8) | function set(sizes) {

FILE: src/renderer/utils.ts
  function handleActions (line 3) | function handleActions(defaultState, handlers) {
  function createAction (line 22) | function createAction(type: string, payloadCreator, metaCreator) {

FILE: src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Config/index.jsx
  class Config (line 10) | class Config extends React.PureComponent {
    method getProp (line 15) | getProp(property) {
    method setProp (line 22) | setProp(property, value) {
    method componentWillReceiveProps (line 29) | componentWillReceiveProps(nextProps) {
    method connect (line 42) | connect() {
    method handleChange (line 53) | handleChange(property, e) {
    method duplicate (line 61) | duplicate() {
    method save (line 73) | save() {
    method renderCertInput (line 80) | renderCertInput(label, id) {
    method render (line 107) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Favorite.jsx
  class Favorite (line 6) | class Favorite extends React.PureComponent {
    method constructor (line 7) | constructor() {
    method _updateSortableKey (line 15) | _updateSortableKey() {
    method _bindSortable (line 19) | _bindSortable() {
    method componentDidMount (line 37) | componentDidMount() {
    method componentDidUpdate (line 41) | componentDidUpdate() {
    method onClick (line 45) | onClick(index, evt) {
    method onDoubleClick (line 50) | onDoubleClick(index, evt) {
    method selectIndex (line 55) | selectIndex(index, connect) {
    method select (line 59) | select(favorite, connect) {
    method render (line 69) | render() {
    method componentWillUnmount (line 127) | componentWillUnmount() {

FILE: src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/index.jsx
  class ConnectionSelector (line 10) | class ConnectionSelector extends PureComponent {
    method handleSelectFavorite (line 13) | handleSelectFavorite(connect, key) {
    method render (line 17) | render() {
  function mapStateToProps (line 47) | function mapStateToProps(state, {instance}) {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/AddButton/index.jsx
  function AddButton (line 5) | function AddButton({title, reload, onReload, onClick}) {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Config/index.jsx
  class Config (line 8) | class Config extends React.Component {
    method constructor (line 9) | constructor(props) {
    method load (line 218) | load() {
    method componentWillUnmount (line 254) | componentWillUnmount() {
    method renderGroup (line 258) | renderGroup(group) {
    method change (line 268) | change({name, value}) {
    method renderConfig (line 278) | renderConfig(config) {
    method isChanged (line 324) | isChanged(rewrite) {
    method handleReload (line 328) | handleReload() {
    method handleSave (line 342) | handleSave() {
    method handleApply (line 358) | handleApply(embed) {
    method render (line 376) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Footer.jsx
  class Footer (line 20) | class Footer extends React.PureComponent {
    method init (line 23) | init(keyName, keyType) {
    method componentDidMount (line 50) | componentDidMount() {
    method componentWillReceiveProps (line 54) | componentWillReceiveProps(nextProps) {
    method render (line 62) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/Editor/index.jsx
  class Editor (line 25) | class Editor extends React.PureComponent {
    method constructor (line 26) | constructor() {
    method updateLayout (line 45) | updateLayout() {
    method componentDidMount (line 59) | componentDidMount() {
    method componentWillUnmount (line 64) | componentWillUnmount() {
    method componentWillReceiveProps (line 68) | componentWillReceiveProps(nextProps) {
    method init (line 74) | init(buffer) {
    method save (line 95) | save() {
    method updateContent (line 120) | updateContent(mode, content) {
    method updateMode (line 127) | updateMode(evt) {
    method focus (line 132) | focus() {
    method handleKeyDown (line 142) | handleKeyDown(evt) {
    method render (line 150) | render() {
  function tryFormatJSON (line 251) | function tryFormatJSON(jsonString, beautify) {
  function tryFormatMessagepack (line 265) | function tryFormatMessagepack(buffer, beautify) {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/HashContent.jsx
  class HashContent (line 13) | class HashContent extends BaseContent {
    method save (line 14) | save(value, callback) {
    method load (line 28) | load(index) {
    method handleSelect (line 50) | handleSelect(evt, selectedIndex) {
    method handleKeyDown (line 57) | handleKeyDown(e) {
    method deleteSelectedMember (line 78) | deleteSelectedMember() {
    method showContextMenu (line 102) | showContextMenu(e, row) {
    method render (line 131) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/ListContent.jsx
  class ListContent (line 12) | class ListContent extends BaseContent {
    method save (line 13) | save(value, callback) {
    method load (line 28) | load(index) {
    method handleSelect (line 71) | handleSelect(_, selectedIndex) {
    method deleteSelectedMember (line 79) | async deleteSelectedMember() {
    method handleKeyDown (line 104) | handleKeyDown(e) {
    method showContextMenu (line 124) | showContextMenu(e, row) {
    method renderEditor (line 138) | renderEditor() {
    method renderIndexColumn (line 149) | renderIndexColumn() {
    method renderValueColumn (line 170) | renderValueColumn() {
    method render (line 218) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/SetContent.jsx
  class SetContent (line 13) | class SetContent extends BaseContent {
    method save (line 14) | save(value, callback) {
    method load (line 38) | load(index) {
    method handleSelect (line 62) | handleSelect(evt, selectedIndex) {
    method handleKeyDown (line 69) | handleKeyDown(e) {
    method deleteSelectedMember (line 90) | deleteSelectedMember() {
    method showContextMenu (line 114) | showContextMenu(e, row) {
    method render (line 128) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/SortHeaderCell.jsx
  function SortHeaderCell (line 6) | function SortHeaderCell({onOrderChange, desc, title}) {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/StringContent.jsx
  class StringContent (line 7) | class StringContent extends BaseContent {
    method init (line 8) | init(keyName, keyType) {
    method save (line 15) | save(value, callback) {
    method create (line 26) | create() {
    method render (line 30) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/ZSetContent.jsx
  class ZSetContent (line 17) | class ZSetContent extends BaseContent {
    method save (line 18) | save(value, callback) {
    method load (line 37) | load(index) {
    method handleSelect (line 72) | handleSelect(_, selectedIndex) {
    method handleKeyDown (line 79) | handleKeyDown(e) {
    method deleteSelectedMember (line 100) | deleteSelectedMember() {
    method showContextMenu (line 124) | showContextMenu(_, row) {
    method renderTable (line 153) | renderTable() {
    method renderScoreColumn (line 175) | renderScoreColumn() {
    method renderMemberColumn (line 223) | renderMemberColumn() {
    method renderEditor (line 283) | renderEditor() {
    method render (line 294) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/index.jsx
  class BaseContent (line 17) | class BaseContent extends React.Component {
    method constructor (line 18) | constructor() {
    method init (line 26) | init(keyName, keyType) {
    method load (line 48) | load(index) {
    method rowClassGetter (line 59) | rowClassGetter(index) {
    method componentDidMount (line 70) | componentDidMount() {
    method componentDidUpdate (line 74) | componentDidUpdate() {
    method componentWillReceiveProps (line 80) | componentWillReceiveProps(nextProps) {
    method componentWillUnmount (line 87) | componentWillUnmount() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/index.jsx
  class KeyContent (line 14) | class KeyContent extends PureComponent {
    method constructor (line 15) | constructor() {
    method render (line 20) | render() {
  function mapStateToProps (line 40) | function mapStateToProps(state) {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/TabBar/index.jsx
  constant TABS (line 6) | const TABS = ['Content', 'Terminal', 'Config']
  function renderTabIcon (line 8) | function renderTabIcon(tab) {
  function renderTab (line 19) | function renderTab(tab, {activeTab, onSelectTab}) {
  function Content (line 30) | function Content(props) {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Terminal/index.jsx
  class Terminal (line 11) | class Terminal extends React.PureComponent {
    method constructor (line 12) | constructor() {
    method componentDidMount (line 17) | componentDidMount() {
    method onSelect (line 88) | onSelect(db) {
    method execute (line 92) | execute(term, args) {
    method componentWillReceiveProps (line 141) | componentWillReceiveProps(nextProps) {
    method componentWillUnmount (line 147) | componentWillUnmount() {
    method render (line 151) | render() {
  function getHTML (line 158) | function getHTML(response) {
  function formatMonitor (line 186) | function formatMonitor(time, args) {
  function formatMessage (line 203) | function formatMessage(channel, message) {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/index.jsx
  class Content (line 10) | class Content extends React.PureComponent {
    method constructor (line 11) | constructor() {
    method init (line 21) | init(keyName) {
    method componentDidMount (line 33) | componentDidMount() {
    method componentWillReceiveProps (line 37) | componentWillReceiveProps(nextProps) {
    method handleTabChange (line 46) | handleTabChange(tab) {
    method render (line 50) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/ContentEditable/index.jsx
  class ContentEditable (line 7) | class ContentEditable extends React.Component {
    method constructor (line 8) | constructor() {
    method render (line 12) | render() {
    method shouldComponentUpdate (line 28) | shouldComponentUpdate(nextProps) {
    method componentDidMount (line 33) | componentDidMount() {
    method componentDidUpdate (line 39) | componentDidUpdate() {
    method handleKeyDown (line 53) | handleKeyDown(evt) {
    method handleChange (line 67) | handleChange(evt) {
    method handleSubmit (line 75) | handleSubmit() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/Footer.jsx
  class Footer (line 5) | class Footer extends React.Component {
    method constructor (line 6) | constructor() {
    method componentDidMount (line 11) | componentDidMount() {
    method componentWillReceiveProps (line 17) | componentWillReceiveProps(nextProps) {
    method updateDBCount (line 23) | updateDBCount() {
    method updateInfo (line 39) | updateInfo() {
    method guessDatabaseNumber (line 58) | guessDatabaseNumber(select, startIndex, lastSuccessIndex) {
    method componentWillUnmount (line 73) | componentWillUnmount() {
    method handleChange (line 78) | handleChange(evt) {
    method keyCountByDb (line 83) | keyCountByDb(dbNumber){
    method render (line 95) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/KeyList/index.jsx
  class KeyList (line 12) | class KeyList extends React.Component {
    method refresh (line 22) | refresh(firstTime) {
    method componentWillReceiveProps (line 32) | componentWillReceiveProps(nextProps) {
    method scan (line 52) | scan(firstTime) {
    method handleSelect (line 167) | handleSelect(index, force) {
    method deleteSelectedKey (line 185) | deleteSelectedKey() {
    method componentDidMount (line 208) | componentDidMount() {
    method setTTLforKey (line 239) | setTTLforKey() {
    method duplicateKey (line 272) | duplicateKey() {
    method createKey (line 302) | createKey(key, type) {
    method showContextMenu (line 318) | showContextMenu(e, row) {
    method render (line 365) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/PatternList/index.jsx
  class PatternList (line 8) | class PatternList extends React.Component {
    method constructor (line 9) | constructor(props) {
    method componentWillReceiveProps (line 17) | componentWillReceiveProps(nextProps) {
    method updatePattern (line 26) | updatePattern(value) {
    method render (line 31) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/index.jsx
  constant FOOTER_HEIGHT (line 9) | const FOOTER_HEIGHT = 66
  function KeyBrowser (line 11) | function KeyBrowser({

FILE: src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/index.jsx
  class Database (line 10) | class Database extends React.PureComponent {
    method constructor (line 11) | constructor() {
    method componentDidMount (line 26) | componentDidMount() {
    method componentWillUnmount (line 32) | componentWillUnmount() {
    method updateLayout (line 36) | updateLayout() {
    method handleCreateKey (line 42) | handleCreateKey(key) {
    method render (line 46) | render() {
  function mapStateToProps (line 85) | function mapStateToProps(state, {instance}) {

FILE: src/renderer/windows/MainWindow/InstanceContent/Modal/index.jsx
  class Modal (line 7) | class Modal extends React.Component {
    method handleSubmit (line 8) | handleSubmit() {
    method handleCancel (line 21) | handleCancel() {
    method componentDidMount (line 25) | componentDidMount() {
    method handleKeyDown (line 46) | handleKeyDown(evt) {
    method render (line 85) | render() {

FILE: src/renderer/windows/MainWindow/InstanceContent/index.jsx
  class InstanceContent (line 9) | class InstanceContent extends PureComponent {
    method constructor (line 10) | constructor() {
    method componentDidMount (line 15) | componentDidMount() {
    method modalSubmit (line 26) | modalSubmit(result) {
    method modalCancel (line 34) | modalCancel() {
    method componentWillUnmount (line 42) | componentWillUnmount() {
    method render (line 46) | render() {

FILE: src/renderer/windows/MainWindow/InstanceTabs/Tab.tsx
  type ITabProps (line 4) | interface ITabProps {
  function Tab (line 12) | function Tab({instanceKey, onTabClick, onTabCloseButtonClick, active, ti...

FILE: src/renderer/windows/MainWindow/InstanceTabs/Tabs.tsx
  type ITabsProps (line 5) | interface ITabsProps {
  function Tabs (line 12) | function Tabs({instances, activeInstanceKey, onTabSelect, onTabClose}: I...

FILE: src/renderer/windows/MainWindow/InstanceTabs/index.tsx
  function isModalShown (line 6) | function isModalShown() {
  type IInstanceTabsProps (line 12) | interface IInstanceTabsProps {
  function InstanceTabs (line 21) | function InstanceTabs({

FILE: src/renderer/windows/MainWindow/index.jsx
  class MainWindow (line 12) | class MainWindow extends PureComponent {
    method componentDidMount (line 13) | componentDidMount() {
    method componentWillUnmount (line 17) | componentWillUnmount() {
    method onHotKey (line 21) | onHotKey(e) {
    method getTitle (line 45) | getTitle() {
    method render (line 57) | render() {

FILE: src/renderer/windows/PatternManagerWindow/index.jsx
  class App (line 10) | class App extends React.Component {
    method constructor (line 11) | constructor(props, context) {
    method handleChange (line 16) | handleChange(property, e) {
    method select (line 20) | select(index) {
    method renderPatternForm (line 28) | renderPatternForm(activePattern) {
    method render (line 71) | render() {
  function mapStateToProps (line 111) | function mapStateToProps(state) {
  function getParameterByName (line 125) | function getParameterByName(name) {
Condensed preview — 92 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (238K chars).
[
  {
    "path": ".babelrc",
    "chars": 351,
    "preview": "{\n  \"ignore\": [\n    \"buffer\"\n  ],\n  \"plugins\": [\n    \"@babel/plugin-proposal-object-rest-spread\",\n    \"@babel/plugin-syn"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 135,
    "preview": "# These are supported funding model platforms\n\ngithub: [luin]\ncustom: https://apps.apple.com/us/app/medis-2-gui-for-redi"
  },
  {
    "path": ".gitignore",
    "chars": 70,
    "preview": "node_modules\n.DS_Store\ndist\nnpm-debug.log\n*.provisionprofile\n.awcache\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 5473,
    "preview": "<a name=\"0.6.1\"></a>\n## [0.6.1](https://github.com/luin/medis/compare/v0.5.0...v0.6.1) (2017-02-19)\n\n\n### Bug Fixes\n\n* d"
  },
  {
    "path": "LICENSE",
    "chars": 1080,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016-2022 Zihua Li\n\nPermission is hereby granted, free of charge, to any person obt"
  },
  {
    "path": "README.md",
    "chars": 4854,
    "preview": "# Medis\n\n### Notice: We just released Medis 2! 🚀🚀🚀\n\nCompared to Medis (this repo), Medis 2 provides more delightful feat"
  },
  {
    "path": "bin/pack.js",
    "chars": 1034,
    "preview": "const packager = require('electron-packager')\nconst path = require('path')\nconst pkg = require('../package')\nconst flat "
  },
  {
    "path": "package.json",
    "chars": 2562,
    "preview": "{\n  \"name\": \"medis\",\n  \"description\": \"GUI for Redis\",\n  \"productName\": \"Medis\",\n  \"version\": \"1.0.3\",\n  \"electronVersio"
  },
  {
    "path": "resources/child.plist",
    "chars": 304,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "resources/icns/generate",
    "chars": 750,
    "preview": "mkdir MyIcon.iconset\nsips -z 16 16     Icon1024.png --out MyIcon.iconset/icon_16x16.png\nsips -z 32 32     Icon1024.png -"
  },
  {
    "path": "resources/parent.plist",
    "chars": 448,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "src/main/index.ts",
    "chars": 897,
    "preview": "import {app, Menu, ipcMain} from 'electron'\nimport windowManager from './windowManager'\nimport menu from './menu'\nconst "
  },
  {
    "path": "src/main/menu.ts",
    "chars": 3989,
    "preview": "import {app, Menu, MenuItemConstructorOptions} from 'electron'\nimport windowManager from './windowManager'\n\nconst menuTe"
  },
  {
    "path": "src/main/windowManager.ts",
    "chars": 1910,
    "preview": "import {app, BrowserWindow, BrowserWindowConstructorOptions} from 'electron'\nimport path from 'path'\nimport EventEmitter"
  },
  {
    "path": "src/renderer/photon/css/photon.css",
    "chars": 32544,
    "preview": "/*!\n * =====================================================\n * Photon v0.1.0\n * Copyright 2015 Connor Sears\n * Licensed"
  },
  {
    "path": "src/renderer/redux/actions/connection.js",
    "chars": 4558,
    "preview": "'use strict';\n\nimport {createAction} from 'Utils';\nimport {Client} from 'ssh2';\nimport net from 'net';\nimport Redis from"
  },
  {
    "path": "src/renderer/redux/actions/favorites.js",
    "chars": 587,
    "preview": "import {createAction} from 'Utils';\nimport {fromJS} from 'immutable'\nimport {Favorites} from '../../storage'\n\n\nexport co"
  },
  {
    "path": "src/renderer/redux/actions/index.js",
    "chars": 136,
    "preview": "export * from './instances'\nexport * from './favorites'\nexport * from './patterns'\nexport * from './connection'\nexport *"
  },
  {
    "path": "src/renderer/redux/actions/instances.js",
    "chars": 1312,
    "preview": "import {createAction} from 'Utils';\nimport {remote} from 'electron'\nimport {getId} from 'Utils'\n\nexport const createInst"
  },
  {
    "path": "src/renderer/redux/actions/patterns.js",
    "chars": 526,
    "preview": "import {createAction} from 'Utils'\nimport {Patterns} from '../../storage'\n\n\nexport const createPattern = createAction('C"
  },
  {
    "path": "src/renderer/redux/actions/sizes.js",
    "chars": 134,
    "preview": "import {createAction} from 'Utils';\n\nexport const setSize = createAction('SET_SIZE', (type, value) => ({type, value: Num"
  },
  {
    "path": "src/renderer/redux/middlewares/createThunkReplyMiddleware.js",
    "chars": 497,
    "preview": "function isThunkReply(action) {\n  return typeof action.payload === 'function' && action.args\n}\n\nexport default function "
  },
  {
    "path": "src/renderer/redux/middlewares/index.js",
    "chars": 107,
    "preview": "import createThunkReplyMiddleware from './createThunkReplyMiddleware'\n\nexport {createThunkReplyMiddleware}\n"
  },
  {
    "path": "src/renderer/redux/persistEnhancer.js",
    "chars": 550,
    "preview": "import * as Storage from '../storage'\n\nconst whiteList = [\n  {key: 'patterns', storage: 'Patterns'},\n  {key: 'favorites'"
  },
  {
    "path": "src/renderer/redux/reducers/activeInstanceKey.js",
    "chars": 585,
    "preview": "import {handleActions} from 'Utils'\nimport {\n  createInstance,\n  selectInstance,\n  moveInstance,\n  delInstance\n} from 'R"
  },
  {
    "path": "src/renderer/redux/reducers/favorites.js",
    "chars": 912,
    "preview": "import {handleActions} from 'Utils'\nimport {\n  createFavorite,\n  removeFavorite,\n  updateFavorite,\n  reorderFavorites,\n "
  },
  {
    "path": "src/renderer/redux/reducers/index.js",
    "chars": 355,
    "preview": "'use strict';\n\nimport {combineReducers} from 'redux';\nimport {activeInstanceKey} from './activeInstanceKey'\nimport {inst"
  },
  {
    "path": "src/renderer/redux/reducers/instances.js",
    "chars": 1723,
    "preview": "import {handleActions} from 'Utils'\nimport {\n  createInstance,\n  moveInstance,\n  delInstance,\n  updateConnectStatus,\n  c"
  },
  {
    "path": "src/renderer/redux/reducers/patterns.js",
    "chars": 1081,
    "preview": "import {handleActions} from 'Utils'\nimport {\n  createPattern,\n  removePattern,\n  updatePattern,\n  reloadPatterns\n} from "
  },
  {
    "path": "src/renderer/redux/reducers/sizes.js",
    "chars": 307,
    "preview": "import {handleActions} from 'Utils'\nimport {\n  setSize\n} from 'Redux/actions'\nimport {Sizes} from '../../storage'\nimport"
  },
  {
    "path": "src/renderer/redux/store.js",
    "chars": 354,
    "preview": "import {compose, createStore, applyMiddleware} from 'redux'\nimport persistEnhancer from './persistEnhancer'\nimport {crea"
  },
  {
    "path": "src/renderer/storage/Favorites.js",
    "chars": 334,
    "preview": "'use strict'\n\nimport {ipcRenderer} from 'electron'\n\nexport function get() {\n  const data = localStorage.getItem('favorit"
  },
  {
    "path": "src/renderer/storage/Patterns.js",
    "chars": 335,
    "preview": "'use strict'\n\nimport {ipcRenderer} from 'electron'\n\nexport function get() {\n  const data = localStorage.getItem('pattern"
  },
  {
    "path": "src/renderer/storage/Sizes.js",
    "chars": 225,
    "preview": "'use strict'\n\nexport function get() {\n  const data = localStorage.getItem('sizes')\n  return data ? JSON.parse(data) : {}"
  },
  {
    "path": "src/renderer/storage/index.js",
    "chars": 150,
    "preview": "import * as Favorites from './Favorites'\nimport * as Patterns from './Patterns'\nimport * as Sizes from './Sizes'\n\nexport"
  },
  {
    "path": "src/renderer/styles/global.scss",
    "chars": 566,
    "preview": "@import \"photon\";\n@import \"native\";\n\nhtml {\n  background: #ececec;\n}\n\nul {\n  margin: 0;\n  padding: 0;\n}\n\n.sidebar {\n  ba"
  },
  {
    "path": "src/renderer/styles/native.scss",
    "chars": 3395,
    "preview": ".nt-box {\n  box-sizing: border-box;\n  position: relative;\n  cursor: default;\n  background-color: rgba(0, 0, 0, .04);\n  b"
  },
  {
    "path": "src/renderer/styles/photon.scss",
    "chars": 672,
    "preview": ".tab-item-btn {\n  width: 30px;\n  flex: none;\n}\n\n.tab-group {\n  background: #b3b1b3;\n}\n\n.nav-group-item:active {\n  backgr"
  },
  {
    "path": "src/renderer/utils.ts",
    "chars": 1068,
    "preview": "import {createAction as _createAction} from 'redux-actions'\n\nexport function handleActions(defaultState, handlers) {\n  r"
  },
  {
    "path": "src/renderer/vendors/jquery.terminal/index.css",
    "chars": 3032,
    "preview": "/*\n * This css file is part of jquery terminal\n *\n * Licensed under GNU LGPL Version 3 license\n * Copyright (c) 2011-201"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Config/index.jsx",
    "chars": 8392,
    "preview": "'use strict'\n\nimport React from 'react'\nimport Immutable from 'immutable'\nimport {remote} from 'electron'\nimport fs from"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Config/index.scss",
    "chars": 512,
    "preview": "#sshPassword {\n  padding-right: 32px;\n}\n\n.ssh-key {\n  height: 22px;\n  line-height: 22px;\n  padding: 0;\n  text-align: cen"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Favorite.jsx",
    "chars": 3538,
    "preview": "'use strict'\n\nimport React from 'react'\nimport Sortable from 'sortablejs'\n\nclass Favorite extends React.PureComponent {\n"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/index.jsx",
    "chars": 1909,
    "preview": "'use strict'\n\nimport React, {PureComponent} from 'react'\nimport {connect} from 'react-redux'\nimport Favorite from './Fav"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/AddButton/index.jsx",
    "chars": 341,
    "preview": "import React, {memo} from 'react'\n\nrequire('./index.scss')\n\nfunction AddButton({title, reload, onReload, onClick}) {\n  r"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/AddButton/index.scss",
    "chars": 485,
    "preview": ".AddButton {\n  position: relative;\n\n  span.plus, span.reload {\n    position: absolute;\n    right: 4px;\n    top: 4px;\n   "
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Config/index.jsx",
    "chars": 12210,
    "preview": "'use strict'\n\nimport React from 'react'\nimport clone from 'lodash.clone'\n\nrequire('./index.scss')\n\nclass Config extends "
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Config/index.scss",
    "chars": 215,
    "preview": ".Config {\n  overflow: auto;\n  .wrapper {\n    width: 430px;\n    margin: 0 auto;\n    .nt-form-row label {\n      width: 170"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Footer.jsx",
    "chars": 1963,
    "preview": "'use strict'\n\nimport React from 'react'\nimport humanFormat from 'human-format'\n\nconst timeScale = new humanFormat.Scale("
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/Editor/index.jsx",
    "chars": 7612,
    "preview": "'use strict'\n\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport Codemirror from 'medis-react-codemirror'"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/Editor/index.scss",
    "chars": 957,
    "preview": ".Editor {\n  position: relative;\n  min-width: 0;\n  textarea {\n    border: none;\n    width: 100%;\n    height: 100%;\n  }\n\n "
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/HashContent.jsx",
    "chars": 8563,
    "preview": "'use strict'\n\nimport React from 'react'\nimport BaseContent from '.'\nimport SplitPane from 'react-split-pane'\nimport {Tab"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/ListContent.jsx",
    "chars": 7198,
    "preview": "'use strict'\n\nimport React from 'react'\nimport BaseContent from '.'\nimport SplitPane from 'react-split-pane'\nimport {Tab"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/SetContent.jsx",
    "chars": 6639,
    "preview": "'use strict'\n\nimport React from 'react'\nimport BaseContent from '.'\nimport SplitPane from 'react-split-pane'\nimport {Tab"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/SortHeaderCell.jsx",
    "chars": 830,
    "preview": "'use strict'\n\nimport React, {memo} from 'react'\nimport {Cell} from 'fixed-data-table-contextmenu'\n\nfunction SortHeaderCe"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/StringContent.jsx",
    "chars": 871,
    "preview": "'use strict'\n\nimport React from 'react'\nimport BaseContent from '.'\nimport Editor from './Editor'\n\nclass StringContent e"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/ZSetContent.jsx",
    "chars": 8874,
    "preview": "'use strict'\n\nimport React from 'react'\nimport BaseContent from '.'\nimport SplitPane from 'react-split-pane'\nimport {Tab"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/index.jsx",
    "chars": 1808,
    "preview": "'use strict'\n\nimport React from 'react'\n\nrequire('./index.scss')\n\nconst getDefaultState = function () {\n  return {\n    k"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/index.scss",
    "chars": 821,
    "preview": ".BaseContent {\n  flex: 1;\n  position: relative;\n\n  .type-list {\n    .index-label {\n      background: #ccc;\n      margin:"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/index.jsx",
    "chars": 1529,
    "preview": "'use strict'\n\nimport React, {PureComponent} from 'react'\nimport {connect} from 'react-redux'\nimport {setSize} from 'Redu"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/index.scss",
    "chars": 212,
    "preview": ".notfound {\n  position: absolute;\n  top: 50%;\n  font-size: 22px;\n  color: #ccc;\n  text-align: center;\n  transform: trans"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/TabBar/index.jsx",
    "chars": 754,
    "preview": "'use strict'\n\nimport React, {memo} from 'react'\nrequire('./index.scss')\n\nconst TABS = ['Content', 'Terminal', 'Config']\n"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/TabBar/index.scss",
    "chars": 225,
    "preview": ".TabBar {\n  text-align: right;\n  border-bottom: 1px solid #d3d3d3;\n  .item {\n    display: inline-block;\n    padding: 12p"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Terminal/index.jsx",
    "chars": 6002,
    "preview": "'use strict'\n\nimport React from 'react'\nimport commands from 'redis-commands'\nimport splitargs from 'redis-splitargs'\nim"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Terminal/index.scss",
    "chars": 945,
    "preview": ".Terminal {\n  overflow: auto;\n  flex: 1;\n  * {\n    -webkit-user-select: text;\n  }\n\n  &.terminal, .cmd {\n    --background"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/index.jsx",
    "chars": 2347,
    "preview": "'use strict'\n\nimport React from 'react'\nimport TabBar from './TabBar'\nimport KeyContent from './KeyContent'\nimport Termi"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/ContentEditable/index.jsx",
    "chars": 1919,
    "preview": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport escape from 'lodash.escape'\n\nrequire('./index.scss')\n\n"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/ContentEditable/index.scss",
    "chars": 104,
    "preview": ".ContentEditable {\n  [contenteditable=\"true\"] {\n    background: #fff !important;\n    color: #333;\n  }\n}\n"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/Footer.jsx",
    "chars": 3079,
    "preview": "'use strict'\n\nimport React from 'react'\n\nclass Footer extends React.Component {\n  constructor() {\n    super()\n    this.s"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/KeyList/index.jsx",
    "chars": 14799,
    "preview": "'use strict'\n\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport {Table, Column} from 'fixed-data-table-c"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/KeyList/index.scss",
    "chars": 1558,
    "preview": ".pattern-table {\n  position: relative;\n  overflow: hidden;\n  &:focus {\n    outline: 0;\n  }\n\n  footer {\n    height: 24px;"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/PatternList/index.jsx",
    "chars": 2211,
    "preview": "'use strict'\n\nimport React from 'react'\nimport {ipcRenderer} from 'electron'\n\nrequire('./index.scss')\n\nclass PatternList"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/PatternList/index.scss",
    "chars": 1396,
    "preview": ".pattern-input {\n  position: relative;\n  padding: 6px;\n  .icon-search {\n    position: absolute;\n    left: 14px;\n    top:"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/index.jsx",
    "chars": 1038,
    "preview": "'use strict'\n\nimport React, {memo} from 'react'\nimport {List} from 'immutable'\nimport PatternList from './PatternList'\ni"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/index.jsx",
    "chars": 2448,
    "preview": "'use strict'\n\nimport React from 'react'\nimport {connect} from 'react-redux'\nimport SplitPane from 'react-split-pane'\nimp"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/index.scss",
    "chars": 954,
    "preview": ".Resizer {\n  background: #000;\n  opacity: .2;\n  z-index: 1;\n  -moz-box-sizing: border-box;\n  -webkit-box-sizing: border-"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/Modal/index.jsx",
    "chars": 3254,
    "preview": "import React from 'react'\nimport ReactDOM from 'react-dom'\nrequire('json-editor')\n\nrequire('./index.scss')\n\nexport defau"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/Modal/index.scss",
    "chars": 2412,
    "preview": ".Modal {\n  position: fixed;\n  left: 0;\n  width: 100%;\n  z-index: 999;\n  height: calc(100% + 100px);\n  margin-top: -100px"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceContent/index.jsx",
    "chars": 2002,
    "preview": "'use strict'\n\nimport React, {PureComponent} from 'react'\nimport ConnectionSelectorContainer from './ConnectionSelectorCo"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceTabs/Tab.tsx",
    "chars": 691,
    "preview": "import React, {memo} from 'react'\nimport {SortableElement} from 'react-sortable-hoc'\n\ninterface ITabProps {\n  instanceKe"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceTabs/Tabs.tsx",
    "chars": 822,
    "preview": "import React, {memo} from 'react'\nimport {SortableContainer} from 'react-sortable-hoc'\nimport Tab from './Tab'\n\ninterfac"
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceTabs/index.tsx",
    "chars": 1876,
    "preview": "import React, {memo} from 'react'\nimport Tabs from './Tabs'\n\nrequire('./main.scss')\n\nfunction isModalShown() {\n  return "
  },
  {
    "path": "src/renderer/windows/MainWindow/InstanceTabs/main.scss",
    "chars": 1074,
    "preview": ".instance-tabs {\n  * {\n    -webkit-user-select: none;\n  }\n  display: flex;\n\n  li {\n    position: relative;\n    flex-grow"
  },
  {
    "path": "src/renderer/windows/MainWindow/entry.jsx",
    "chars": 742,
    "preview": "'use strict'\n\nrequire('../../photon/css/photon.min.css')\nrequire('../../../../node_modules/fixed-data-table-contextmenu/"
  },
  {
    "path": "src/renderer/windows/MainWindow/index.jsx",
    "chars": 2693,
    "preview": "'use strict'\n\nimport React, {PureComponent} from 'react'\nimport {createSelector} from 'reselect'\nimport {Provider, conne"
  },
  {
    "path": "src/renderer/windows/PatternManagerWindow/app.scss",
    "chars": 1065,
    "preview": ".patternList {\n  background: #fff;\n  border: 1px solid #c5c5c5;\n  width: 210px;\n  position: absolute;\n  top: 20px;\n  lef"
  },
  {
    "path": "src/renderer/windows/PatternManagerWindow/entry.jsx",
    "chars": 768,
    "preview": "'use strict'\n\nrequire('../../photon/css/photon.min.css')\n\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimp"
  },
  {
    "path": "src/renderer/windows/PatternManagerWindow/index.jsx",
    "chars": 3890,
    "preview": "import React from 'react'\nimport {connect} from 'react-redux'\nimport {createPattern, updatePattern, removePattern} from "
  },
  {
    "path": "tsconfig.json",
    "chars": 382,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"target\": \"es6\",\n    \"lib\": [\n      \"es6\"\n    ],\n    \"moduleResolution"
  },
  {
    "path": "webpack.config.js",
    "chars": 3013,
    "preview": "'use strict';\n\nconst {resolve} = require('path')\nconst {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')\nconst"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the luin/medis GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 92 files (214.7 KB), approximately 62.9k tokens, and a symbol index with 271 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!