├── .babelrc
├── .github
└── FUNDING.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bin
└── pack.js
├── package-lock.json
├── package.json
├── resources
├── child.plist
├── dockface.png
├── icns
│ ├── Icon1024.png
│ ├── MyIcon.icns
│ └── generate
├── parent.plist
└── upstash.png
├── src
├── main
│ ├── index.ts
│ ├── menu.ts
│ └── windowManager.ts
└── renderer
│ ├── images
│ └── design.sketch
│ ├── photon
│ ├── css
│ │ ├── photon.css
│ │ └── photon.min.css
│ └── fonts
│ │ ├── photon-entypo.eot
│ │ ├── photon-entypo.svg
│ │ ├── photon-entypo.ttf
│ │ └── photon-entypo.woff
│ ├── 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
│ │ │ ├── icon.png
│ │ │ ├── index.jsx
│ │ │ ├── index.scss
│ │ │ └── warning.png
│ │ └── 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
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": [
3 | "buffer"
4 | ],
5 | "plugins": [
6 | "@babel/plugin-proposal-object-rest-spread",
7 | "@babel/plugin-syntax-dynamic-import",
8 | "@babel/plugin-proposal-class-properties"
9 | ],
10 | "presets": [
11 | [
12 | "@babel/preset-env",
13 | {
14 | "targets": {
15 | "chrome": "69"
16 | }
17 | }
18 | ],
19 | "@babel/preset-react"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [luin]
4 | custom: https://apps.apple.com/us/app/medis-2-gui-for-redis/id1579200037
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | npm-debug.log
5 | *.provisionprofile
6 | .awcache
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## [0.6.1](https://github.com/luin/medis/compare/v0.5.0...v0.6.1) (2017-02-19)
3 |
4 |
5 | ### Bug Fixes
6 |
7 | * 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)
8 | * UI for edit button ([3599392](https://github.com/luin/medis/commit/3599392))
9 | * zset delete wrong element when sorting desc ([3d3f29a](https://github.com/luin/medis/commit/3d3f29a)), closes [#60](https://github.com/luin/medis/issues/60)
10 |
11 | ### Features
12 |
13 | * support search/find within a key ([9ecce73](https://github.com/luin/medis/commit/9ecce73)), closes [#61](https://github.com/luin/medis/issues/61)
14 |
15 |
16 |
17 |
18 | # [0.6.0](https://github.com/luin/medis/compare/v0.5.0...v0.6.0) (2017-02-19)
19 |
20 |
21 | ### Bug Fixes
22 |
23 | * 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)
24 | * UI for edit button ([3599392](https://github.com/luin/medis/commit/3599392))
25 | * zset delete wrong element when sorting desc ([3d3f29a](https://github.com/luin/medis/commit/3d3f29a)), closes [#60](https://github.com/luin/medis/issues/60)
26 |
27 | ### Features
28 |
29 | * support search/find within a key ([9ecce73](https://github.com/luin/medis/commit/9ecce73)), closes [#61](https://github.com/luin/medis/issues/61)
30 |
31 |
32 |
33 |
34 | # [0.5.0](https://github.com/luin/medis/compare/v0.3.0...v0.5.0) (2016-12-04)
35 |
36 |
37 | ### Bug Fixes
38 |
39 | * check err first before update the database status ([dd46cc3](https://github.com/luin/medis/commit/dd46cc3))
40 | * clear the state before leaving the favorite page ([498a077](https://github.com/luin/medis/commit/498a077))
41 | * don't show error multiple times when lost connection to SSH tunnel ([2b732bd](https://github.com/luin/medis/commit/2b732bd))
42 | * fix psubscribe not working. Close #32 ([586a943](https://github.com/luin/medis/commit/586a943)), closes [#32](https://github.com/luin/medis/issues/32)
43 | * provide details error when connection is failed ([99d2757](https://github.com/luin/medis/commit/99d2757))
44 | * tweak config panel style ([d92faf2](https://github.com/luin/medis/commit/d92faf2))
45 | * ui issues when switching between tabs ([330f52f](https://github.com/luin/medis/commit/330f52f)), closes [#1](https://github.com/luin/medis/issues/1)
46 |
47 | ### Features
48 |
49 | * add support for SSL connection. ([ca29384](https://github.com/luin/medis/commit/ca29384)), closes [#41](https://github.com/luin/medis/issues/41)
50 | * allow quick connecting by double clicking ([53a284e](https://github.com/luin/medis/commit/53a284e))
51 | * support Elastic Cache Redis & RedisLabs for selecting database ([18e5629](https://github.com/luin/medis/commit/18e5629))
52 | * support to duplicate favorites ([c2bc438](https://github.com/luin/medis/commit/c2bc438)), closes [#30](https://github.com/luin/medis/issues/30)
53 | * 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)
54 |
55 |
56 |
57 |
58 | # [0.5.0](https://github.com/luin/medis/compare/v0.3.0...v0.5.0) (2016-12-04)
59 |
60 |
61 | ### Bug Fixes
62 |
63 | * check err first before update the database status ([dd46cc3](https://github.com/luin/medis/commit/dd46cc3))
64 | * clear the state before leaving the favorite page ([498a077](https://github.com/luin/medis/commit/498a077))
65 | * don't show error multiple times when lost connection to SSH tunnel ([2b732bd](https://github.com/luin/medis/commit/2b732bd))
66 | * fix psubscribe not working. Close #32 ([586a943](https://github.com/luin/medis/commit/586a943)), closes [#32](https://github.com/luin/medis/issues/32)
67 | * provide details error when connection is failed ([99d2757](https://github.com/luin/medis/commit/99d2757))
68 | * tweak config panel style ([d92faf2](https://github.com/luin/medis/commit/d92faf2))
69 | * ui issues when switching between tabs ([330f52f](https://github.com/luin/medis/commit/330f52f)), closes [#1](https://github.com/luin/medis/issues/1)
70 |
71 | ### Features
72 |
73 | * add support for SSL connection. ([ca29384](https://github.com/luin/medis/commit/ca29384)), closes [#41](https://github.com/luin/medis/issues/41)
74 | * allow quick connecting by double clicking ([53a284e](https://github.com/luin/medis/commit/53a284e))
75 | * support Elastic Cache Redis & RedisLabs for selecting database ([18e5629](https://github.com/luin/medis/commit/18e5629))
76 | * support to duplicate favorites ([c2bc438](https://github.com/luin/medis/commit/c2bc438)), closes [#30](https://github.com/luin/medis/issues/30)
77 | * 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)
78 |
79 |
80 |
81 |
82 | # [0.3.0](https://github.com/luin/medis/compare/v0.2.1...v0.3.0) (2016-03-25)
83 |
84 |
85 | ### Bug Fixes
86 |
87 | * **windows:** hide app menu in Windows version ([d31bd6c](https://github.com/luin/medis/commit/d31bd6c))
88 |
89 | ### Features
90 |
91 | * support inputing spaces in terminal ([04e7bcf](https://github.com/luin/medis/commit/04e7bcf)), closes [#24](https://github.com/luin/medis/issues/24)
92 |
93 |
94 |
95 |
96 | ## [0.2.1](https://github.com/luin/medis/compare/v0.2.0...v0.2.1) (2016-02-01)
97 |
98 |
99 | ### Bug Fixes
100 |
101 | * **ssh:** fix ssh password being ignored ([4dfdbcd](https://github.com/luin/medis/commit/4dfdbcd)), closes [#13](https://github.com/luin/medis/issues/13)
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016-2022 Zihua Li
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Medis
2 |
3 | ### Notice: We just released Medis 2! 🚀🚀🚀
4 |
5 | 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!
6 |
7 | What's more, **Medis 2 is free 💰 to download**! Don't hesitate, download it from the App Store now and try it out!
8 |
9 | [](https://apps.apple.com/us/app/medis-2-gui-for-redis/id1579200037?mt=12)
10 |
11 | _(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/))_
12 |
13 | 
14 |
15 | ---
16 |
17 | 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).
18 |
19 | [](http://commitizen.github.io/cz-cli/)
20 |
21 | Medis starts with all the basic features you need:
22 |
23 | - Keys viewing/editing
24 | - SSH Tunnel for connecting with remote servers
25 | - Terminal for executing custom commands
26 | - Config viewing/editing
27 |
28 | It also supports many advanced features:
29 |
30 | - JSON/MessagePack format viewing/editing and built-in highlighting/validator
31 | - Working with millions keys and key members without blocking the redis server
32 | - Pattern manager for easy selecting a sub group of keys.
33 |
34 | **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.
35 |
36 |
37 |
38 | ## Download Medis on Windows
39 |
40 | You can download compiled installer of Medis for Windows from the below page
41 | [download page](https://github.com/classfellow/medis/releases/tag/win)
42 |
43 | ## Download Medis on Mac
44 |
45 | You can download compiled versions of Medis for Mac OS X from [the release page](https://github.com/luin/medis/releases).
46 |
47 | ## Running Locally
48 |
49 | 1. Install dependencies
50 |
51 | ```
52 | $ npm install
53 | ```
54 |
55 | 2. Compile assets:
56 |
57 | ```
58 | $ npm run pack
59 | ```
60 |
61 | 3. Run with Electron:
62 |
63 | ```
64 | $ npm start
65 | ```
66 |
67 | ## Connect to Heroku
68 |
69 | 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:
70 |
71 | ```shell
72 | $ heroku redis:credentials --app YOUR_APP
73 | redis://x:PASSWORD@HOST:PORT
74 | ```
75 |
76 | And then input `HOST`, `PORT` and `PASSWORD` to the connection tab.
77 |
78 | ## I Love This. How do I Help?
79 |
80 | - Simply star this repository :-)
81 | - Help us spread the world on Facebook and Twitter
82 | - Contribute Code! We're developers! (See Roadmap below)
83 | - Medis is available on the Mac App Store as a paid software. I'll be very grateful if you'd like to buy it to encourage me to continue maintaining Medis. There are no additional features comparing with the open-sourced version, except the fact that you can enjoy auto updating that brought by the Mac App Store. [](https://apps.apple.com/us/app/medis-2-gui-for-redis/id1579200037?mt=12)
84 |
85 | ## Roadmap
86 |
87 | - Windows and Linux version (with electron-packager)
88 | - Support for SaaS Redis services
89 | - Lua script editor
90 | - Cluster management
91 | - GEO keys supporting
92 |
93 | ## Contributors
94 |
95 | luin
kvnsmth
dpde
ogasawaraShinnosuke
naholyr
hlobil
Janpot
96 |
97 | ## License
98 |
99 | MIT
100 |
--------------------------------------------------------------------------------
/bin/pack.js:
--------------------------------------------------------------------------------
1 | const packager = require('electron-packager')
2 | const path = require('path')
3 | const pkg = require('../package')
4 | const flat = require('electron-osx-sign').flat
5 |
6 | const resourcesPath = path.join(__dirname, '..', 'resources')
7 |
8 | packager({
9 | dir: path.join(__dirname, '..'),
10 | appCopyright: '© 2019, Zihua Li',
11 | asar: true,
12 | overwrite: true,
13 | electronVersion: pkg.electronVersion,
14 | icon: path.join(resourcesPath, 'icns', 'MyIcon'),
15 | out: path.join(__dirname, '..', 'dist', 'out'),
16 | platform: 'mas',
17 | appBundleId: `li.zihua.${pkg.name}`,
18 | appCategoryType: 'public.app-category.developer-tools',
19 | osxSign: {
20 | type: process.env.NODE_ENV === 'production' ? 'distribution' : 'development',
21 | entitlements: path.join(resourcesPath, 'parent.plist'),
22 | 'entitlements-inherit': path.join(resourcesPath, 'child.plist')
23 | }
24 | }).then((res) => {
25 | const app = path.join(res[0], `${pkg.productName}.app`)
26 | console.log('flating...', app)
27 | flat({ app }, function done (err) {
28 | if (err) {
29 | throw err
30 | }
31 | process.exit(0);
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "medis",
3 | "description": "GUI for Redis",
4 | "productName": "Medis",
5 | "version": "1.0.3",
6 | "electronVersion": "4.0.0",
7 | "license": "MIT",
8 | "author": "luin (http://zihua.li)",
9 | "main": "dist/main",
10 | "scripts": {
11 | "build": "rm -rf dist && webpack",
12 | "watch": "WEBPACK_WATCH=true npm run build",
13 | "start": "electron .",
14 | "pack": "NODE_ENV=production npm run build && node bin/pack.js"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/luin/medis.git"
19 | },
20 | "dependencies": {
21 | "electron": "^4.2.2",
22 | "electron-context-menu": "^0.12.1",
23 | "fixed-data-table-contextmenu": "^1.7.2",
24 | "ioredis": "^4.9.3",
25 | "jquery": "^3.4.1",
26 | "jquery.terminal": "^2.5.1",
27 | "lodash.escape": "^4.0.1",
28 | "lodash.sortedindexby": "^4.6.0",
29 | "medis-react-codemirror": "^1.1.1",
30 | "redis-commands": "^1.5.0",
31 | "ssh2": "^0.8.9",
32 | "xterm": "^3.13.1"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "^7.4.4",
36 | "@babel/plugin-proposal-class-properties": "^7.4.4",
37 | "@babel/plugin-proposal-object-rest-spread": "^7.4.4",
38 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
39 | "@babel/preset-env": "^7.4.4",
40 | "@babel/preset-react": "^7.0.0",
41 | "@types/jquery": "^3.3.29",
42 | "@types/react": "^16.8.17",
43 | "@types/react-dom": "^16.8.4",
44 | "@types/redux-actions": "^2.6.1",
45 | "awesome-typescript-loader": "^5.2.1",
46 | "babel-loader": "^8.0.6",
47 | "codemirror": "^5.46.0",
48 | "css-loader": "^2.1.1",
49 | "electron-osx-sign": "^0.4.4",
50 | "electron-packager": "^13.1.1",
51 | "file-loader": "^3.0.1",
52 | "html-webpack-plugin": "^3.2.0",
53 | "human-format": "^0.10.1",
54 | "immutable": "^3.8.1",
55 | "json-editor": "^0.7.23",
56 | "jsonlint": "^1.6.2",
57 | "jsx-loader": "^0.13.2",
58 | "lint": "^1.1.2",
59 | "lodash.clone": "^4.5.0",
60 | "lodash.zip": "^4.2.0",
61 | "mini-css-extract-plugin": "^0.6.0",
62 | "minimatch": "^3.0.4",
63 | "msgpack5": "^4.2.1",
64 | "node-sass": "^4.12.0",
65 | "prop-types": "^15.7.2",
66 | "react": "^16.8.6",
67 | "react-addons-css-transition-group": "^15.5.2",
68 | "react-document-title": "^2.0.1",
69 | "react-dom": "^16.8.6",
70 | "react-redux": "^7.0.3",
71 | "react-sortable-hoc": "^1.9.1",
72 | "react-split-pane": "^0.1.87",
73 | "redis-splitargs": "^1.0.1",
74 | "redux": "^4.0.1",
75 | "redux-actions": "^2.6.5",
76 | "reselect": "^4.0.0",
77 | "sass-loader": "^7.1.0",
78 | "sortablejs": "^1.9.0",
79 | "typescript": "^3.4.5",
80 | "url-loader": "^1.1.2",
81 | "webpack": "^4.31.0",
82 | "webpack-bundle-analyzer": "^3.3.2",
83 | "webpack-cli": "^3.3.2"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/resources/child.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.inherit
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/resources/dockface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/resources/dockface.png
--------------------------------------------------------------------------------
/resources/icns/Icon1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/resources/icns/Icon1024.png
--------------------------------------------------------------------------------
/resources/icns/MyIcon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/resources/icns/MyIcon.icns
--------------------------------------------------------------------------------
/resources/icns/generate:
--------------------------------------------------------------------------------
1 | mkdir MyIcon.iconset
2 | sips -z 16 16 Icon1024.png --out MyIcon.iconset/icon_16x16.png
3 | sips -z 32 32 Icon1024.png --out MyIcon.iconset/icon_16x16@2x.png
4 | sips -z 32 32 Icon1024.png --out MyIcon.iconset/icon_32x32.png
5 | sips -z 64 64 Icon1024.png --out MyIcon.iconset/icon_32x32@2x.png
6 | sips -z 128 128 Icon1024.png --out MyIcon.iconset/icon_128x128.png
7 | sips -z 256 256 Icon1024.png --out MyIcon.iconset/icon_128x128@2x.png
8 | sips -z 256 256 Icon1024.png --out MyIcon.iconset/icon_256x256.png
9 | sips -z 512 512 Icon1024.png --out MyIcon.iconset/icon_256x256@2x.png
10 | sips -z 512 512 Icon1024.png --out MyIcon.iconset/icon_512x512.png
11 | cp Icon1024.png MyIcon.iconset/icon_512x512@2x.png
12 | iconutil -c icns MyIcon.iconset
13 | rm -R MyIcon.iconset
14 |
--------------------------------------------------------------------------------
/resources/parent.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 | com.apple.security.network.client
10 |
11 | com.apple.security.network.server
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/resources/upstash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/resources/upstash.png
--------------------------------------------------------------------------------
/src/main/index.ts:
--------------------------------------------------------------------------------
1 | import {app, Menu, ipcMain} from 'electron'
2 | import windowManager from './windowManager'
3 | import menu from './menu'
4 | const contextMenu = require('electron-context-menu');
5 |
6 | contextMenu({
7 | // showInspectElement: true,
8 | })
9 |
10 | ipcMain.on('create patternManager', function (event, arg) {
11 | windowManager.create('patternManager', arg)
12 | })
13 |
14 | ipcMain.on('dispatch', function (event, action, arg) {
15 | windowManager.dispatch(action, arg)
16 | })
17 |
18 | // Quit when all windows are closed.
19 | app.on('window-all-closed', function () {
20 | if (process.platform !== 'darwin') {
21 | app.quit()
22 | }
23 | })
24 |
25 | // This method will be called when Electron has finished
26 | // initialization and is ready to create browser windows.
27 | app.on('ready', function () {
28 | Menu.setApplicationMenu(menu)
29 | windowManager.create()
30 |
31 | app.on('activate', function (_, hasVisibleWindows) {
32 | if (!hasVisibleWindows) {
33 | windowManager.create()
34 | }
35 | })
36 | })
37 |
--------------------------------------------------------------------------------
/src/main/menu.ts:
--------------------------------------------------------------------------------
1 | import {app, Menu, MenuItemConstructorOptions} from 'electron'
2 | import windowManager from './windowManager'
3 |
4 | const menuTemplates: MenuItemConstructorOptions[] = [{
5 | label: 'File',
6 | submenu: [{
7 | label: 'New Connection Window',
8 | accelerator: 'CmdOrCtrl+N',
9 | click() {
10 | windowManager.create()
11 | }
12 | }, {
13 | label: 'New Connection Tab',
14 | accelerator: 'CmdOrCtrl+T',
15 | click() {
16 | windowManager.current.webContents.send('action', 'createInstance')
17 | }
18 | }, {
19 | type: 'separator'
20 | }, {
21 | label: 'Close Window',
22 | accelerator: 'Shift+CmdOrCtrl+W',
23 | click() {
24 | windowManager.current.close()
25 | }
26 | }, {
27 | label: 'Close Tab',
28 | accelerator: 'CmdOrCtrl+W',
29 | click() {
30 | windowManager.current.webContents.send('action', 'delInstance')
31 | }
32 | }]
33 | }, {
34 | label: 'Edit',
35 | submenu: [{
36 | label: 'Undo',
37 | accelerator: 'CmdOrCtrl+Z',
38 | role: 'undo'
39 | }, {
40 | label: 'Redo',
41 | accelerator: 'Shift+CmdOrCtrl+Z',
42 | role: 'redo'
43 | }, {
44 | type: 'separator'
45 | }, {
46 | label: 'Cut',
47 | accelerator: 'CmdOrCtrl+X',
48 | role: 'cut'
49 | }, {
50 | label: 'Copy',
51 | accelerator: 'CmdOrCtrl+C',
52 | role: 'copy'
53 | }, {
54 | label: 'Paste',
55 | accelerator: 'CmdOrCtrl+V',
56 | role: 'paste'
57 | }, {
58 | label: 'Select All',
59 | accelerator: 'CmdOrCtrl+A',
60 | role: 'selectall'
61 | }]
62 | }, {
63 | label: 'View',
64 | submenu: [{
65 | label: 'Reload',
66 | accelerator: 'CmdOrCtrl+R',
67 | click(item, focusedWindow) {
68 | if (focusedWindow) {
69 | focusedWindow.reload()
70 | }
71 | }
72 | }, {
73 | label: 'Toggle Full Screen',
74 | accelerator: (function () {
75 | if (process.platform === 'darwin') {
76 | return 'Ctrl+Command+F'
77 | }
78 | return 'F11'
79 | })(),
80 | click(item, focusedWindow) {
81 | if (focusedWindow) {
82 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
83 | }
84 | }
85 | }, {
86 | label: 'Toggle Developer Tools',
87 | accelerator: (function () {
88 | if (process.platform === 'darwin') {
89 | return 'Alt+Command+I'
90 | }
91 | return 'Ctrl+Shift+I'
92 | })(),
93 | click(item, focusedWindow) {
94 | if (focusedWindow) {
95 | focusedWindow.webContents.toggleDevTools()
96 | }
97 | }
98 | }]
99 | }, {
100 | label: 'Window',
101 | role: 'window',
102 | submenu: [{
103 | label: 'Minimize',
104 | accelerator: 'CmdOrCtrl+M',
105 | role: 'minimize'
106 | }, {
107 | label: 'Close',
108 | accelerator: 'CmdOrCtrl+W',
109 | role: 'close'
110 | }]
111 | }, {
112 | label: 'Help',
113 | role: 'help',
114 | submenu: [{
115 | label: 'Report an Issue...',
116 | click() {
117 | require('shell').openExternal('mailto:medis@zihua.li')
118 | }
119 | }, {
120 | label: 'Learn More',
121 | click() {
122 | require('shell').openExternal('http://getmedis.com')
123 | }
124 | }]
125 | }]
126 |
127 | let baseIndex = 0
128 | if (process.platform == 'darwin') {
129 | baseIndex = 1
130 | menuTemplates.unshift({
131 | label: app.getName(),
132 | submenu: [{
133 | label: 'About ' + app.getName(),
134 | role: 'about'
135 | }, {
136 | type: 'separator'
137 | }, {
138 | label: 'Services',
139 | role: 'services',
140 | submenu: []
141 | }, {
142 | type: 'separator'
143 | }, {
144 | label: 'Hide ' + app.getName(),
145 | accelerator: 'Command+H',
146 | role: 'hide'
147 | }, {
148 | label: 'Hide Others',
149 | accelerator: 'Command+Shift+H',
150 | role: 'hideothers'
151 | }, {
152 | label: 'Show All',
153 | role: 'unhide'
154 | }, {
155 | type: 'separator'
156 | }, {
157 | label: 'Quit',
158 | accelerator: 'Command+Q',
159 | click() {
160 | app.quit()
161 | }
162 | }]
163 | })
164 | }
165 |
166 | const menu = Menu.buildFromTemplate(menuTemplates)
167 |
168 | if (process.env.NODE_ENV === 'production') {
169 | const {submenu} = (menu.items[baseIndex + 2] as any)
170 | submenu.items[0].visible = false
171 | submenu.items[2].visible = false
172 | }
173 |
174 | const {submenu} = (menu.items[baseIndex + 0] as any)
175 | windowManager.on('blur', function () {
176 | submenu.items[3].enabled = false
177 | submenu.items[4].enabled = false
178 | })
179 |
180 | windowManager.on('focus', function () {
181 | const {submenu} = (menu.items[baseIndex + 0] as any)
182 | submenu.items[3].enabled = true
183 | submenu.items[4].enabled = true
184 | })
185 |
186 | export default menu
187 |
--------------------------------------------------------------------------------
/src/main/windowManager.ts:
--------------------------------------------------------------------------------
1 | import {app, BrowserWindow, BrowserWindowConstructorOptions} from 'electron'
2 | import path from 'path'
3 | import EventEmitter from 'events'
4 |
5 | class WindowManager extends EventEmitter {
6 | windows = new Set()
7 |
8 | constructor() {
9 | super()
10 | app.on('browser-window-blur', this.emit.bind(this, 'blur'))
11 | app.on('browser-window-focus', this.emit.bind(this, 'focus'))
12 | }
13 |
14 | get current() {
15 | return BrowserWindow.getFocusedWindow() || this.create()
16 | }
17 |
18 | create(type = 'main', arg?: any): BrowserWindow {
19 | const option: BrowserWindowConstructorOptions = {
20 | backgroundColor: '#ececec',
21 | webPreferences: {
22 | nodeIntegration: true
23 | }
24 | }
25 | if (type === 'main') {
26 | option.width = 960
27 | option.height = 600
28 | option.show = false
29 | option.minWidth = 840
30 | option.minHeight = 400
31 | } else if (type === 'patternManager') {
32 | option.width = 600
33 | option.height = 300
34 | option.title = 'Manage Patterns'
35 | option.resizable = true
36 | option.fullscreen = false
37 | }
38 |
39 | let start: number
40 | const newWindow = new BrowserWindow(option)
41 | if (!option.show) {
42 | newWindow.once('ready-to-show', () => {
43 | console.log('start time: ', Date.now() - start)
44 | newWindow.show()
45 | })
46 | }
47 |
48 | start = Date.now()
49 | newWindow.loadFile(path.resolve(__dirname, `../renderer/${type}.html`), {query: {arg}})
50 |
51 | this._register(newWindow)
52 |
53 | return newWindow
54 | }
55 |
56 | _register(win: BrowserWindow): void {
57 | this.windows.add(win)
58 | win.on('closed', () => {
59 | this.windows.delete(win)
60 | if (!BrowserWindow.getFocusedWindow()) {
61 | this.emit('blur')
62 | }
63 | })
64 | this.emit('focus')
65 | }
66 |
67 | dispatch(action: string, args: any) {
68 | this.windows.forEach(win => {
69 | if (win && win.webContents) {
70 | win.webContents.send('action', action, args)
71 | }
72 | })
73 | }
74 | }
75 |
76 | export default new WindowManager()
77 |
--------------------------------------------------------------------------------
/src/renderer/images/design.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/src/renderer/images/design.sketch
--------------------------------------------------------------------------------
/src/renderer/photon/fonts/photon-entypo.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/src/renderer/photon/fonts/photon-entypo.eot
--------------------------------------------------------------------------------
/src/renderer/photon/fonts/photon-entypo.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/src/renderer/photon/fonts/photon-entypo.ttf
--------------------------------------------------------------------------------
/src/renderer/photon/fonts/photon-entypo.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/src/renderer/photon/fonts/photon-entypo.woff
--------------------------------------------------------------------------------
/src/renderer/redux/actions/connection.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {createAction} from 'Utils';
4 | import {Client} from 'ssh2';
5 | import net from 'net';
6 | import Redis from 'ioredis';
7 |
8 | function getIndex(getState) {
9 | const {activeInstanceKey, instances} = getState()
10 | return instances.findIndex(instance => instance.get('key') === activeInstanceKey)
11 | }
12 |
13 | export const updateConnectStatus = createAction('UPDATE_CONNECT_STATUS', status => ({getState, next}) => {
14 | next({status, index: getIndex(getState)})
15 | })
16 |
17 | export const disconnect = createAction('DISCONNECT', () => ({getState, next}) => {
18 | next({index: getIndex(getState)})
19 | })
20 |
21 | export const connectToRedis = createAction('CONNECT', config => ({getState, dispatch, next}) => {
22 | let sshErrorThrown = false
23 | let redisErrorMessage
24 |
25 | if (config.ssh) {
26 | dispatch(updateConnectStatus('SSH connecting...'))
27 |
28 | const conn = new Client();
29 | conn.on('ready', () => {
30 | const server = net.createServer(function (sock) {
31 | conn.forwardOut(sock.remoteAddress, sock.remotePort, config.host, config.port, (err, stream) => {
32 | if (err) {
33 | sock.end()
34 | } else {
35 | sock.pipe(stream).pipe(sock)
36 | }
37 | })
38 | }).listen(0, function () {
39 | handleRedis(config, { host: '127.0.0.1', port: server.address().port })
40 | })
41 | }).on('error', err => {
42 | sshErrorThrown = true;
43 | dispatch(disconnect());
44 | alert(`SSH Error: ${err.message}`);
45 | })
46 |
47 | try {
48 | const connectionConfig = {
49 | host: config.sshHost,
50 | port: config.sshPort || 22,
51 | username: config.sshUser
52 | }
53 | if (config.sshKey) {
54 | conn.connect(Object.assign(connectionConfig, {
55 | privateKey: config.sshKey,
56 | passphrase: config.sshKeyPassphrase
57 | }))
58 | } else {
59 | conn.connect(Object.assign(connectionConfig, {
60 | password: config.sshPassword
61 | }))
62 | }
63 | } catch (err) {
64 | dispatch(disconnect());
65 | alert(`SSH Error: ${err.message}`);
66 | }
67 | } else {
68 | handleRedis(config);
69 | }
70 |
71 | function handleRedis(config, override) {
72 | dispatch(updateConnectStatus('Redis connecting...'))
73 | if (config.ssl) {
74 | config.tls = {
75 | rejectUnauthorized: false
76 | };
77 | if (config.tlsca) config.tls.ca = config.tlsca;
78 | if (config.tlskey) config.tls.key = config.tlskey;
79 | if (config.tlscert) config.tls.cert = config.tlscert;
80 | }
81 | const redis = new Redis(Object.assign({}, config, override, {
82 | retryStrategy() {
83 | return false;
84 | }
85 | }));
86 | redis.defineCommand('setKeepTTL', {
87 | numberOfKeys: 1,
88 | 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'
89 | });
90 | redis.defineCommand('lremindex', {
91 | numberOfKeys: 1,
92 | lua: 'local FLAG = "$$#__@DELETE@_REDIS_@PRO@__#$$" redis.call("lset", KEYS[1], ARGV[1], FLAG) redis.call("lrem", KEYS[1], 1, FLAG)'
93 | });
94 | redis.defineCommand('duplicateKey', {
95 | numberOfKeys: 2,
96 | 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)'
97 | });
98 | redis.once('connect', function () {
99 | redis.ping((err, res) => {
100 | if (err) {
101 | if (err.message === 'Ready check failed: NOAUTH Authentication required.') {
102 | err.message = 'Redis Error: Access denied. Please double-check your password.';
103 | }
104 | if (err.message !== 'Connection is closed.') {
105 | alert(err.message);
106 | redis.disconnect();
107 | }
108 | return;
109 | }
110 | const version = redis.serverInfo.redis_version;
111 | if (version && version.length >= 5) {
112 | const versionNumber = Number(version[0] + version[2]);
113 | if (versionNumber < 28) {
114 | 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.');
115 | dispatch(disconnect());
116 | return;
117 | }
118 | }
119 | next({redis, config, index: getIndex(getState)});
120 | })
121 | });
122 | redis.once('error', function (error) {
123 | redisErrorMessage = error;
124 | });
125 | redis.once('end', function () {
126 | dispatch(disconnect());
127 | if (!sshErrorThrown) {
128 | let msg = 'Redis Error: Connection failed. ';
129 | if (redisErrorMessage) {
130 | msg += `(${redisErrorMessage})`;
131 | }
132 | alert(msg);
133 | }
134 | });
135 | }
136 | })
137 |
--------------------------------------------------------------------------------
/src/renderer/redux/actions/favorites.js:
--------------------------------------------------------------------------------
1 | import {createAction} from 'Utils';
2 | import {fromJS} from 'immutable'
3 | import {Favorites} from '../../storage'
4 |
5 |
6 | export const createFavorite = createAction('CREATE_FAVORITE', (data) => {
7 | const key = `favorite-${Math.round(Math.random() * 100000)}`
8 | return Object.assign({key}, data)
9 | })
10 |
11 | export const reloadFavorites = createAction('RELOAD_FAVORITES', Favorites.get)
12 | export const removeFavorite = createAction('REMOVE_FAVORITE')
13 | export const reorderFavorites = createAction('REORDER_FAVORITES')
14 | export const updateFavorite = createAction('UPDATE_FAVORITE', (key, data) => ({key, data}))
15 |
--------------------------------------------------------------------------------
/src/renderer/redux/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from './instances'
2 | export * from './favorites'
3 | export * from './patterns'
4 | export * from './connection'
5 | export * from './sizes'
6 |
--------------------------------------------------------------------------------
/src/renderer/redux/actions/instances.js:
--------------------------------------------------------------------------------
1 | import {createAction} from 'Utils';
2 | import {remote} from 'electron'
3 | import {getId} from 'Utils'
4 |
5 | export const createInstance = createAction('CREATE_INSTANCE', data => (
6 | Object.assign({}, data, {key: getId('instance')})
7 | ))
8 |
9 | export const selectInstance = createAction('SELECT_INSTANCE')
10 |
11 | export const moveInstance = createAction('MOVE_INSTANCE', (from, to) => ({getState, next}) => {
12 | const {instances} = getState()
13 |
14 | const [fromIndex, instance] = instances.findEntry(v => v.get('key') === from);
15 | const toIndex = instances.findIndex(v => v.get('key') === to);
16 |
17 | next({fromIndex, toIndex, activeInstanceKey: instance.get('key')})
18 | })
19 |
20 | export const delInstance = createAction('DEL_INSTANCE', key => ({getState, next}) => {
21 | const {activeInstanceKey, instances} = getState()
22 | if (!key) {
23 | key = activeInstanceKey
24 | }
25 |
26 | const targetIndex = instances.findIndex(instance => instance.get('key') === key);
27 |
28 | const ret = {activeInstanceKey, targetIndex}
29 |
30 | if (key === activeInstanceKey) {
31 | const item = instances.get(targetIndex + 1) || (targetIndex > 0 && instances.get(targetIndex - 1))
32 |
33 | console.log('still', item, targetIndex, instances.size)
34 | if (item) {
35 | ret.activeInstanceKey = item.get('key')
36 | } else {
37 | remote.getCurrentWindow().close();
38 | return;
39 | }
40 | }
41 |
42 | next(ret)
43 | })
44 |
--------------------------------------------------------------------------------
/src/renderer/redux/actions/patterns.js:
--------------------------------------------------------------------------------
1 | import {createAction} from 'Utils'
2 | import {Patterns} from '../../storage'
3 |
4 |
5 | export const createPattern = createAction('CREATE_PATTERN', (conn) => {
6 | const key = `pattern-${Math.round(Math.random() * 100000)}`
7 | return Object.assign({key, conn})
8 | })
9 |
10 | export const reloadPatterns = createAction('RELOAD_PATTERNS', Patterns.get)
11 | export const removePattern = createAction('REMOVE_PATTERN', (conn, index) => ({conn, index}))
12 | export const updatePattern = createAction('UPDATE_PATTERN', (conn, index, data) => ({conn, index, data}))
13 |
--------------------------------------------------------------------------------
/src/renderer/redux/actions/sizes.js:
--------------------------------------------------------------------------------
1 | import {createAction} from 'Utils';
2 |
3 | export const setSize = createAction('SET_SIZE', (type, value) => ({type, value: Number(value)}))
4 |
--------------------------------------------------------------------------------
/src/renderer/redux/middlewares/createThunkReplyMiddleware.js:
--------------------------------------------------------------------------------
1 | function isThunkReply(action) {
2 | return typeof action.payload === 'function' && action.args
3 | }
4 |
5 | export default function createThunkReplyMiddleware(extraArgument) {
6 | return function ({dispatch, getState}) {
7 | return _next => action => {
8 | if (!isThunkReply(action)) {
9 | return _next(action)
10 | }
11 |
12 | function next(payload) {
13 | dispatch({payload, type: action.type})
14 | }
15 |
16 | return action.payload(Object.assign({dispatch, getState, next}, extraArgument))
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/renderer/redux/middlewares/index.js:
--------------------------------------------------------------------------------
1 | import createThunkReplyMiddleware from './createThunkReplyMiddleware'
2 |
3 | export {createThunkReplyMiddleware}
4 |
--------------------------------------------------------------------------------
/src/renderer/redux/persistEnhancer.js:
--------------------------------------------------------------------------------
1 | import * as Storage from '../storage'
2 |
3 | const whiteList = [
4 | {key: 'patterns', storage: 'Patterns'},
5 | {key: 'favorites', storage: 'Favorites'},
6 | {key: 'sizes', storage: 'Sizes'}
7 | ]
8 |
9 | export default function (store) {
10 | let lastState
11 | store.subscribe(() => {
12 | if (store.skipPersist) {
13 | return
14 | }
15 | const state = store.getState()
16 | whiteList.forEach(({key, storage}) => {
17 | const value = state[key]
18 | if (!lastState || value !== lastState[key]) {
19 | Storage[storage].set(value)
20 | }
21 | })
22 |
23 | lastState = state
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/renderer/redux/reducers/activeInstanceKey.js:
--------------------------------------------------------------------------------
1 | import {handleActions} from 'Utils'
2 | import {
3 | createInstance,
4 | selectInstance,
5 | moveInstance,
6 | delInstance
7 | } from 'Redux/actions'
8 |
9 | export const defaultInstanceKey = 'FIRST_INSTANCE'
10 |
11 | export const activeInstanceKey = handleActions(defaultInstanceKey, {
12 | [createInstance](state, data) {
13 | return data.key
14 | },
15 | [selectInstance](state, data) {
16 | return data
17 | },
18 | [moveInstance](state, {activeInstanceKey}) {
19 | return activeInstanceKey
20 | },
21 | [delInstance](state, {activeInstanceKey}) {
22 | console.log('==delInstance', activeInstanceKey)
23 | return activeInstanceKey
24 | }
25 | })
26 |
--------------------------------------------------------------------------------
/src/renderer/redux/reducers/favorites.js:
--------------------------------------------------------------------------------
1 | import {handleActions} from 'Utils'
2 | import {
3 | createFavorite,
4 | removeFavorite,
5 | updateFavorite,
6 | reorderFavorites,
7 | reloadFavorites
8 | } from 'Redux/actions'
9 | import {Favorites} from '../../storage'
10 | import {Map, fromJS} from 'immutable'
11 |
12 | function FavoriteFactory(data) {
13 | return Map(Object.assign({name: 'New Favorite'}, data))
14 | }
15 |
16 | export const favorites = handleActions(fromJS(Favorites.get()), {
17 | [createFavorite](state, data) {
18 | return state.push(FavoriteFactory(data))
19 | },
20 | [removeFavorite](state, key) {
21 | return state.filterNot(item => item.get('key') === key)
22 | },
23 | [updateFavorite](state, {key, data}) {
24 | return state.map(item => item.get('key') === key ? item.merge(data) : item)
25 | },
26 | [reorderFavorites](state, {from, to}) {
27 | const target = state.get(from);
28 | return state.splice(from, 1).splice(to, 0, target);
29 | },
30 | [reloadFavorites](state, data) {
31 | return fromJS(data)
32 | }
33 | })
34 |
--------------------------------------------------------------------------------
/src/renderer/redux/reducers/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {combineReducers} from 'redux';
4 | import {activeInstanceKey} from './activeInstanceKey'
5 | import {instances} from './instances'
6 | import {favorites} from './favorites'
7 | import {patterns} from './patterns'
8 | import {sizes} from './sizes'
9 |
10 | export default combineReducers({
11 | patterns,
12 | favorites,
13 | instances,
14 | activeInstanceKey,
15 | sizes
16 | });
17 |
--------------------------------------------------------------------------------
/src/renderer/redux/reducers/instances.js:
--------------------------------------------------------------------------------
1 | import {handleActions} from 'Utils'
2 | import {
3 | createInstance,
4 | moveInstance,
5 | delInstance,
6 | updateConnectStatus,
7 | connectToRedis,
8 | disconnect
9 | } from 'Redux/actions'
10 | import {Map, List} from 'immutable'
11 | import {defaultInstanceKey} from './activeInstanceKey'
12 |
13 | function InstanceFactory({key, data}) {
14 | return Map(Object.assign({key, title: 'Medis'}, data))
15 | }
16 |
17 | export const instances = handleActions(List([InstanceFactory({key: defaultInstanceKey})]), {
18 | [createInstance](state, data) {
19 | return state.push(InstanceFactory({data}))
20 | },
21 | [moveInstance](state, {fromIndex, toIndex}) {
22 | const instance = state.get(fromIndex)
23 | return state.splice(fromIndex, 1).splice(toIndex, 0, instance)
24 | },
25 | [delInstance](state, {targetIndex}) {
26 | return state.remove(targetIndex)
27 | },
28 | [updateConnectStatus](state, {index, status}) {
29 | return state.setIn([index, 'connectStatus'], status)
30 | },
31 | [disconnect](state, {index}) {
32 | const properties = ['connectStatus', 'redis', 'config', 'version']
33 | return state.update(index, instance => (
34 | instance.withMutations(map => {
35 | properties.forEach(key => map.remove(key))
36 | map.set('title', 'Medis')
37 | })
38 | ))
39 | },
40 | [connectToRedis](state, {index, config, redis}) {
41 | const {name, sshHost, host, port} = config
42 | const remote = name ? `${name}/` : (sshHost ? `${sshHost}/` : '')
43 | const address = `${host}:${port}`
44 | const title = `${remote}${address}`
45 | const connectionKey = `${sshHost || ''}|${host}|${port}`
46 | const version = redis.serverInfo && redis.serverInfo.redis_version
47 |
48 | return state.update(index, instance => (
49 | instance.merge({config, title, redis, version, connectionKey}).remove('connectStatus')
50 | ))
51 | }
52 | })
53 |
--------------------------------------------------------------------------------
/src/renderer/redux/reducers/patterns.js:
--------------------------------------------------------------------------------
1 | import {handleActions} from 'Utils'
2 | import {
3 | createPattern,
4 | removePattern,
5 | updatePattern,
6 | reloadPatterns
7 | } from 'Redux/actions'
8 | import {Patterns} from '../../storage'
9 | import {Map, List, fromJS} from 'immutable'
10 |
11 | function PatternFactory(data) {
12 | return Map(Object.assign({value: '*', name: '*'}, data))
13 | }
14 |
15 | export const patterns = handleActions(fromJS(Patterns.get()), {
16 | [createPattern](state, {conn, key}) {
17 | return state.update(conn, List(), patterns => patterns.push(PatternFactory({key})))
18 | },
19 | [removePattern](state, {conn, index}) {
20 | return state.update(conn, List(), patterns => patterns.remove(index))
21 | },
22 | [updatePattern](state, {conn, index, data}) {
23 | return state.update(conn, List(), patterns => patterns.update(index, item => item.merge(data)))
24 | },
25 | // [reorderPatterns](state, {conn, from, to}) {
26 | // return state.update(conn, List(), patterns => {
27 | // const target = patterns.get(from);
28 | // return patterns.splice(from, 1).splice(to, 0, target);
29 | // })
30 | // },
31 | [reloadPatterns](state, data) {
32 | return fromJS(data)
33 | }
34 | })
35 |
--------------------------------------------------------------------------------
/src/renderer/redux/reducers/sizes.js:
--------------------------------------------------------------------------------
1 | import {handleActions} from 'Utils'
2 | import {
3 | setSize
4 | } from 'Redux/actions'
5 | import {Sizes} from '../../storage'
6 | import {Map, List, fromJS} from 'immutable'
7 |
8 | export const sizes = handleActions(fromJS(Sizes.get()), {
9 | [setSize](state, {type, value}) {
10 | return state.set(`${type}BarWidth`, value)
11 | }
12 | })
13 |
--------------------------------------------------------------------------------
/src/renderer/redux/store.js:
--------------------------------------------------------------------------------
1 | import {compose, createStore, applyMiddleware} from 'redux'
2 | import persistEnhancer from './persistEnhancer'
3 | import {createThunkReplyMiddleware} from 'Redux/middlewares'
4 | import reducers from './reducers'
5 |
6 | const store = window.store = createStore(
7 | reducers,
8 | applyMiddleware(createThunkReplyMiddleware())
9 | )
10 |
11 | persistEnhancer(store)
12 |
13 | export default store
14 |
--------------------------------------------------------------------------------
/src/renderer/storage/Favorites.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import {ipcRenderer} from 'electron'
4 |
5 | export function get() {
6 | const data = localStorage.getItem('favorites')
7 | return data ? JSON.parse(data) : []
8 | }
9 |
10 | export function set(favorites) {
11 | localStorage.setItem('favorites', JSON.stringify(favorites))
12 |
13 | ipcRenderer.send('dispatch', 'reloadFavorites')
14 | return favorites
15 | }
16 |
--------------------------------------------------------------------------------
/src/renderer/storage/Patterns.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import {ipcRenderer} from 'electron'
4 |
5 | export function get() {
6 | const data = localStorage.getItem('patternStore')
7 | return data ? JSON.parse(data) : {}
8 | }
9 |
10 | export function set(patterns) {
11 | localStorage.setItem('patternStore', JSON.stringify(patterns))
12 | ipcRenderer.send('dispatch', 'reloadPatterns')
13 | return patterns
14 | }
15 |
--------------------------------------------------------------------------------
/src/renderer/storage/Sizes.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | export function get() {
4 | const data = localStorage.getItem('sizes')
5 | return data ? JSON.parse(data) : {}
6 | }
7 |
8 | export function set(sizes) {
9 | localStorage.setItem('sizes', JSON.stringify(sizes))
10 | return sizes
11 | }
12 |
--------------------------------------------------------------------------------
/src/renderer/storage/index.js:
--------------------------------------------------------------------------------
1 | import * as Favorites from './Favorites'
2 | import * as Patterns from './Patterns'
3 | import * as Sizes from './Sizes'
4 |
5 | export {Favorites, Patterns, Sizes}
6 |
--------------------------------------------------------------------------------
/src/renderer/styles/global.scss:
--------------------------------------------------------------------------------
1 | @import "photon";
2 | @import "native";
3 |
4 | html {
5 | background: #ececec;
6 | }
7 |
8 | ul {
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
13 | .sidebar {
14 | background: #f5f5f4;
15 | display: flex;
16 | flex-direction: column;
17 |
18 | .nav-group {
19 | overflow: auto;
20 | flex: 1;
21 | }
22 | }
23 |
24 | .main {
25 | position: relative;
26 | flex: 1;
27 | }
28 |
29 | .nav-group {
30 | .sortable-placeholder {
31 | width: 100%;
32 | height: 26px;
33 | }
34 | }
35 |
36 | textarea {
37 | &:focus {
38 | outline: none;
39 | }
40 | }
41 |
42 | .Pane.vertical {
43 | height: 100%;
44 | display: flex;
45 | min-width: 0;
46 | }
47 |
48 | .fixedDataTableCellLayout_columnResizerContainer {
49 | border-right: 1px solid #ccc;
50 | }
51 |
--------------------------------------------------------------------------------
/src/renderer/styles/native.scss:
--------------------------------------------------------------------------------
1 | .nt-box {
2 | box-sizing: border-box;
3 | position: relative;
4 | cursor: default;
5 | background-color: rgba(0, 0, 0, .04);
6 | border-width: 1px;
7 | border-style: solid;
8 | border-top-color: rgba(0, 0, 0, .07);
9 | border-left-color: rgba(0, 0, 0, .037);
10 | border-right-color: rgba(0, 0, 0, .037);
11 | border-bottom-color: rgba(0, 0, 0, .026);
12 | border-radius: 4px;
13 | padding: 23px 18px 22px 18px;
14 | }
15 |
16 | .nt-form-row, .form-control {
17 | padding: 5px 0;
18 | $label-width: 140px;
19 | label {
20 | float: left;
21 | width: $label-width;
22 | text-align: right;
23 | -webkit-user-select: text;
24 | }
25 | input, textarea, select {
26 | margin-left: 10px;
27 | }
28 | input[type="text"], input[type="number"], input[type="password"], select {
29 | width: 250px;
30 | -webkit-user-select: text;
31 | border-width: 1px;
32 | border-style: solid;
33 | border-color: #b0b0b0;
34 | border-left-color: #b1b1b1;
35 | border-right-color: #b1b1b1;
36 | box-shadow: inset 0 0 0 1px #f0f0f0;
37 | padding-top: 4px;
38 | padding-bottom: 4px;
39 | padding-left: 3.5px;
40 | padding-right: 3.5px;
41 | line-height: 14px;
42 | font-family: "San Francisco", "Helvetica Neue", "Lucida Grande", Arial, sans-serif;
43 | font-size: 13px;
44 | background: #fff;
45 |
46 | &:focus {
47 | outline: none;
48 | border-color: #6691d6;
49 | box-shadow: 0 0 0 2.5px #7ba7ec;
50 | border-radius: 1px;
51 | }
52 |
53 | &:placeholder {
54 | color: #c0c0c0;
55 | }
56 |
57 | &[disabled] {
58 | background: #f8f8f8;
59 | }
60 | }
61 |
62 | input[type="radio"],
63 | input[type="checkbox"] {
64 | line-height: normal;
65 | }
66 |
67 | &.nt-form-row--vertical {
68 | overflow: visible;
69 | label {
70 | float: none;
71 | display: block;
72 | text-align: left;
73 | }
74 | input[type="text"], input[type="password"], textarea {
75 | margin-left: 0;
76 | width: 100%;
77 | }
78 | }
79 | }
80 |
81 | .nt-button {
82 | cursor: default;
83 | background-color: #ffffff;
84 | outline: none;
85 | border-width: 1px;
86 | border-style: solid;
87 | border-radius: 3px;
88 | border-top-color: #c8c8c8;
89 | border-bottom-color: #acacac;
90 | border-left-color: #c2c2c2;
91 | border-right-color: #c2c2c2;
92 | box-shadow: 0 1px rgba(0, 0, 0, .039);
93 | padding-top: 0;
94 | padding-bottom: 0;
95 | padding-left: 13px;
96 | padding-right: 13px;
97 | line-height: 19px;
98 | font-size: 13px;
99 |
100 | &:active {
101 | background-image: -webkit-linear-gradient(top, #4c98fe 0%, #0564e3 100%);
102 | border-top-color: #247fff;
103 | border-bottom-color: #003ddb;
104 | border-left-color: #125eed;
105 | border-right-color: #125eed;
106 | color: rgba(255, 255, 255, .9);
107 | }
108 |
109 | margin-right: 10px;
110 | &:last-child {
111 | margin-right: 0;
112 | }
113 | }
114 |
115 | .nt-button--primary {
116 | background-image: -webkit-linear-gradient(top, #6ca5fc 0%, #076aff 100%);
117 | border-top-color: #4c93fa;
118 | border-bottom-color: #0261ff;
119 | border-left-color: #2d7efc;
120 | border-right-color: #2d7efc;
121 | color: rgba(255, 255, 255, .9);
122 |
123 | &:active {
124 | background-image: -webkit-linear-gradient(top, #4c98fe 0%, #0564e3 100%);
125 | border-top-color: #247fff;
126 | border-bottom-color: #003ddb;
127 | border-left-color: #125eed;
128 | border-right-color: #125eed;
129 | color: rgba(255, 255, 255, .9);
130 | }
131 | }
132 |
133 | .nt-button:focus {
134 | outline: none;
135 | border-color: #6691d6;
136 | box-shadow: 0 0 0 2.5px #7ba7ec;
137 | }
138 |
139 | .nt-button[disabled], .nt-button--disabled {
140 | background: #f1f1f1;
141 | border-color: #d0d0d0;
142 | color: #b1b1b1;
143 | }
144 |
145 | .nt-button-group {
146 | text-align: center;
147 |
148 | .nt-button {
149 | display: inline-block;
150 | }
151 | &.nt-button-group--pull-right {
152 | text-align: right;
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/renderer/styles/photon.scss:
--------------------------------------------------------------------------------
1 | .tab-item-btn {
2 | width: 30px;
3 | flex: none;
4 | }
5 |
6 | .tab-group {
7 | background: #b3b1b3;
8 | }
9 |
10 | .nav-group-item:active {
11 | background-color: transparent !important;
12 | }
13 |
14 | .nav-group-item.sortable-ghost {
15 | opacity: 0;
16 | }
17 |
18 | .nav-group-item.active:active {
19 | background-color: #dcdfe1 !important;
20 | }
21 |
22 | .window {
23 | background: #ececec !important;
24 | }
25 |
26 | .toolbar-footer {
27 | button {
28 | width: 30px;
29 | border: none;
30 | border-right: 1px solid #c2c0c2;
31 | box-shadow: 1px 0px 0px 0px rgba(255, 255, 255, 0.4);
32 | background: transparent;
33 | font-size: 18px;
34 | line-height: 19px;
35 | opacity: 0.8;
36 | &:active {
37 | background-color: #dcdfe1;
38 | }
39 | }
40 | }
41 |
42 | button:focus {
43 | outline:0;
44 | }
45 |
--------------------------------------------------------------------------------
/src/renderer/utils.ts:
--------------------------------------------------------------------------------
1 | import {createAction as _createAction} from 'redux-actions'
2 |
3 | export function handleActions(defaultState, handlers) {
4 | return function (state = defaultState, {type, payload}) {
5 | const handler = handlers[type]
6 | return handler ? handler(state, payload) : state
7 | }
8 | }
9 |
10 | export const getId = (function () {
11 | const ids = {}
12 |
13 | return function (item: string) {
14 | if (!ids[item]) {
15 | ids[item] = 0
16 | }
17 |
18 | return `${item}-${++ids[item] + (Math.random() * 100000 | 0)}`
19 | }
20 | }())
21 |
22 | export function createAction(type: string, payloadCreator, metaCreator) {
23 | type = `$SOS_${type}`
24 | const actionCreator = _createAction(type, payloadCreator, metaCreator)
25 | const creator = (...args) => {
26 | const action = actionCreator(...args)
27 | if (typeof action.payload === 'function') {
28 | return Object.assign(action, {args})
29 | }
30 | return action
31 | }
32 |
33 | return Object.assign(creator, {
34 | toString: actionCreator.toString,
35 | payload(payload) {
36 | return {type, payload}
37 | },
38 | reply(args, result) {
39 | return {type, payload: {args, result}}
40 | }
41 | })
42 | }
43 |
--------------------------------------------------------------------------------
/src/renderer/vendors/jquery.terminal/index.css:
--------------------------------------------------------------------------------
1 | /*
2 | * This css file is part of jquery terminal
3 | *
4 | * Licensed under GNU LGPL Version 3 license
5 | * Copyright (c) 2011-2013 Jakub Jankiewicz
6 | *
7 | */
8 | .terminal .terminal-output .format, .cmd .format,
9 | .cmd .prompt, .cmd .prompt div, .terminal .terminal-output div div{
10 | display: inline-block;
11 | }
12 | .cmd .clipboard {
13 | position: absolute;
14 | bottom: 0;
15 | left: 0;
16 | opacity: 0.01;
17 | filter: alpha(opacity = 0.01);
18 | filter: progid:DXImageTransform.Microsoft.Alpha(opacity=0.01);
19 | width: 2px;
20 | }
21 | .cmd > .clipboard {
22 | position: fixed;
23 | }
24 | .terminal {
25 | padding: 10px;
26 | position: relative;
27 | overflow: hidden;
28 | }
29 | .cmd {
30 | padding: 0;
31 | margin: 0;
32 | height: 1.3em;
33 | /*margin-top: 3px; */
34 | }
35 | .cmd .cursor.blink {
36 | -webkit-animation: blink 1s infinite steps(1, start);
37 | -moz-animation: blink 1s infinite steps(1, start);
38 | -ms-animation: blink 1s infinite steps(1, start);
39 | animation: blink 1s infinite steps(1, start);
40 | }
41 | @keyframes blink {
42 | 0%, 100% {
43 | background-color: #000;
44 | color: #aaa;
45 | }
46 | 50% {
47 | background-color: #bbb; /* not #aaa because it's seem there is Google Chrome bug */
48 | color: #000;
49 | }
50 | }
51 | @-webkit-keyframes blink {
52 | 0%, 100% {
53 | background-color: #000;
54 | color: #aaa;
55 | }
56 | 50% {
57 | background-color: #bbb;
58 | color: #000;
59 | }
60 | }
61 | @-ms-keyframes blink {
62 | 0%, 100% {
63 | background-color: #000;
64 | color: #aaa;
65 | }
66 | 50% {
67 | background-color: #bbb;
68 | color: #000;
69 | }
70 | }
71 | @-moz-keyframes blink {
72 | 0%, 100% {
73 | background-color: #000;
74 | color: #aaa;
75 | }
76 | 50% {
77 | background-color: #bbb;
78 | color: #000;
79 | }
80 | }
81 | .terminal .terminal-output div div, .cmd .prompt {
82 | display: block;
83 | line-height: 18px;
84 | height: auto;
85 | }
86 | .cmd .prompt {
87 | float: left;
88 | }
89 | .terminal, .cmd {
90 | font-family: monospace;
91 | color: #eed1b3;
92 | background-color: #202020;
93 | font-size: 14px;
94 | line-height: 18px;
95 | }
96 | .terminal-output > div {
97 | /*padding-top: 3px;*/
98 | min-height: 14px;
99 | }
100 | .terminal .terminal-output div span {
101 | display: inline-block;
102 | }
103 | .cmd span {
104 | float: left;
105 | /*display: inline-block; */
106 | }
107 | .terminal .inverted, .cmd .inverted, .cmd .cursor.blink {
108 | background-color: #aaa;
109 | color: #000;
110 | }
111 | .terminal .terminal-output div div::-moz-selection,
112 | .terminal .terminal-output div span::-moz-selection,
113 | .terminal .terminal-output div div a::-moz-selection {
114 | background-color: #aaa;
115 | color: #000;
116 | }
117 | .terminal .terminal-output div div::selection,
118 | .terminal .terminal-output div div a::selection,
119 | .terminal .terminal-output div span::selection,
120 | .cmd > span::selection,
121 | .cmd .prompt span::selection {
122 | background-color: #aaa;
123 | color: #000;
124 | }
125 | .terminal .terminal-output div.error, .terminal .terminal-output div.error div {
126 | color: red;
127 | }
128 | .tilda {
129 | position: fixed;
130 | top: 0;
131 | left: 0;
132 | width: 100%;
133 | z-index: 1100;
134 | }
135 | .clear {
136 | clear: both;
137 | }
138 | .terminal a {
139 | color: #0F60FF;
140 | }
141 | .terminal a:hover {
142 | color: red;
143 | }
144 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Config/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import Immutable from 'immutable'
5 | import {remote} from 'electron'
6 | import fs from 'fs'
7 |
8 | require('./index.scss')
9 |
10 | class Config extends React.PureComponent {
11 | state = {
12 | data: new Immutable.Map()
13 | }
14 |
15 | getProp(property) {
16 | if (this.state.data.has(property)) {
17 | return this.state.data.get(property)
18 | }
19 | return (this.props.favorite ? this.props.favorite.get(property) : '') || ''
20 | }
21 |
22 | setProp(property, value) {
23 | this.setState({
24 | data: typeof property === 'string' ? this.state.data.set(property, value) : this.state.data.merge(property),
25 | changed: Boolean(this.props.favorite)
26 | })
27 | }
28 |
29 | componentWillReceiveProps(nextProps) {
30 | if (!this.props.connect && nextProps.connect) {
31 | this.connect()
32 | }
33 | if (this.props.favorite || nextProps.favorite) {
34 | const leaving = !this.props.favorite || !nextProps.favorite ||
35 | (this.props.favorite.get('key') !== nextProps.favorite.get('key'))
36 | if (leaving) {
37 | this.setState({changed: false, data: new Immutable.Map()})
38 | }
39 | }
40 | }
41 |
42 | connect() {
43 | const {favorite, connectToRedis} = this.props
44 | const data = this.state.data
45 | const config = favorite ? favorite.merge(data).toJS() : data.toJS()
46 | config.host = config.host || 'localhost'
47 | config.port = config.port || '6379'
48 | config.sshPort = config.sshPort || '22'
49 | connectToRedis(config)
50 | this.save()
51 | }
52 |
53 | handleChange(property, e) {
54 | let value = e.target.value
55 | if (property === 'ssh' || property === 'ssl') {
56 | value = e.target.checked
57 | }
58 | this.setProp(property, value)
59 | }
60 |
61 | duplicate() {
62 | if (this.props.favorite) {
63 | const data = Object.assign(this.props.favorite.toJS(), this.state.data.toJS())
64 | delete data.key
65 | this.props.onDuplicate(data)
66 | } else {
67 | const data = this.state.data.toJS()
68 | data.name = 'Quick Connect'
69 | this.props.onDuplicate(data)
70 | }
71 | }
72 |
73 | save() {
74 | if (this.props.favorite && this.state.changed) {
75 | this.props.onSave(this.state.data.toJS())
76 | this.setState({changed: false, data: new Immutable.Map()})
77 | }
78 | }
79 |
80 | renderCertInput(label, id) {
81 | return (
82 | {label}:
83 |
90 | {
93 | const win = remote.getCurrentWindow()
94 | const files = remote.dialog.showOpenDialog(win, {
95 | properties: ['openFile']
96 | })
97 | if (files && files.length) {
98 | const file = files[0]
99 | const content = fs.readFileSync(file, 'utf8')
100 | this.setProp({[id]: content, [`${id}File`]: file})
101 | }
102 | }}
103 | />
104 |
)
105 | }
106 |
107 | render() {
108 | return (
109 |
191 |
192 | {
194 | this.duplicate()
195 | }}
196 | >{this.props.favorite ? 'Duplicate' : 'Add to Favorite'}
197 | {
201 | this.save()
202 | }}
203 | >Save Changes
204 | {
206 | this.connect()
207 | }}
208 | >{this.props.connectStatus || (this.state.changed ? 'Save and Connect' : 'Connect')}
209 |
210 |
)
211 | }
212 | }
213 |
214 | export default Config
215 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Config/index.scss:
--------------------------------------------------------------------------------
1 | #sshPassword {
2 | padding-right: 32px;
3 | }
4 |
5 | .ssh-key {
6 | height: 22px;
7 | line-height: 22px;
8 | padding: 0;
9 | text-align: center;
10 | width: 30px;
11 | position: relative;
12 | top: 0;
13 | left: -30px;
14 | border-top: 0;
15 | border-bottom: 0;
16 | border-right: 1px solid #b1b1b1;
17 | border-left: 1px solid #b1b1b1;
18 | background: linear-gradient(to bottom, #fafafa 0%, #f5f5f5 100%);
19 |
20 | &.is-active {
21 | color: #388af8;
22 | }
23 |
24 | &:active {
25 | background: linear-gradient(to bottom, #c2c2c2 0%, #b6b5b6 100%);
26 | color: #388af8;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/Favorite.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import Sortable from 'sortablejs'
5 |
6 | class Favorite extends React.PureComponent {
7 | constructor() {
8 | super()
9 | this.state = {
10 | activeKey: null
11 | }
12 | this._updateSortableKey()
13 | }
14 |
15 | _updateSortableKey() {
16 | this.sortableKey = `sortable-${Math.round(Math.random() * 10000)}`
17 | }
18 |
19 | _bindSortable() {
20 | const {reorderFavorites} = this.props
21 |
22 | this.sortable = Sortable.create(this.refs.sortable, {
23 | animation: 100,
24 | onStart: evt => {
25 | this.nextSibling = evt.item.nextElementSibling
26 | },
27 | onAdd: () => {
28 | this._updateSortableKey()
29 | },
30 | onUpdate: evt => {
31 | this._updateSortableKey()
32 | reorderFavorites({from: evt.oldIndex, to: evt.newIndex})
33 | }
34 | })
35 | }
36 |
37 | componentDidMount() {
38 | this._bindSortable()
39 | }
40 |
41 | componentDidUpdate() {
42 | this._bindSortable()
43 | }
44 |
45 | onClick(index, evt) {
46 | evt.preventDefault()
47 | this.selectIndex(index)
48 | }
49 |
50 | onDoubleClick(index, evt) {
51 | evt.preventDefault()
52 | this.selectIndex(index, true)
53 | }
54 |
55 | selectIndex(index, connect) {
56 | this.select(index === -1 ? null : this.props.favorites.get(index), connect)
57 | }
58 |
59 | select(favorite, connect) {
60 | const activeKey = favorite ? favorite.get('key') : null
61 | this.setState({activeKey})
62 | if (connect) {
63 | this.props.onRequireConnecting(activeKey)
64 | } else {
65 | this.props.onSelect(activeKey)
66 | }
67 | }
68 |
69 | render() {
70 | return (
71 |
72 |
73 |
78 |
79 | QUICK CONNECT
80 |
81 | FAVORITES
82 |
95 |
96 |
97 | {
99 | this.props.createFavorite()
100 | // TODO: auto select
101 | // this.select(favorite);
102 | }}
103 | >+
104 | {
107 | const key = this.state.activeKey
108 | if (!key) {
109 | return
110 | }
111 | showModal({
112 | title: 'Delete the bookmark?',
113 | button: 'Delete',
114 | content: 'Are you sure you want to delete the selected bookmark? This action cannot be undone.'
115 | }).then(() => {
116 | const index = this.props.favorites.findIndex(favorite => key === favorite.get('key'))
117 | this.props.removeFavorite(key)
118 | this.selectIndex(index - 1)
119 | })
120 | }
121 | }
122 | >-
123 |
124 |
)
125 | }
126 |
127 | componentWillUnmount() {
128 | this.sortable.destroy()
129 | }
130 | }
131 |
132 | export default Favorite
133 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/ConnectionSelectorContainer/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {PureComponent} from 'react'
4 | import {connect} from 'react-redux'
5 | import Favorite from './Favorite'
6 | import Config from './Config'
7 | import {connectToRedis} from 'Redux/actions'
8 | import {removeFavorite, updateFavorite, createFavorite, reorderFavorites} from 'Redux/actions'
9 |
10 | class ConnectionSelector extends PureComponent {
11 | state = {connect: false, key: null}
12 |
13 | handleSelectFavorite(connect, key) {
14 | this.setState({connect, key})
15 | }
16 |
17 | render() {
18 | const selectedFavorite = this.state.key && this.props.favorites.find(item => item.get('key') === this.state.key)
19 | return (
20 |
31 |
32 | {
38 | this.props.updateFavorite(selectedFavorite.get('key'), data)
39 | }}
40 | onDuplicate={this.props.createFavorite}
41 | />
42 |
43 |
)
44 | }
45 | }
46 |
47 | function mapStateToProps(state, {instance}) {
48 | return {
49 | favorites: state.favorites,
50 | connectStatus: instance.get('connectStatus')
51 | }
52 | }
53 | const mapDispatchToProps = {
54 | updateFavorite,
55 | createFavorite,
56 | connectToRedis,
57 | reorderFavorites,
58 | removeFavorite
59 | }
60 |
61 | export default connect(mapStateToProps, mapDispatchToProps)(ConnectionSelector)
62 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/AddButton/index.jsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react'
2 |
3 | require('./index.scss')
4 |
5 | function AddButton({title, reload, onReload, onClick}) {
6 | return (
7 | {title}
8 | {reload && }
9 | +
10 |
)
11 | }
12 |
13 | export default memo(AddButton)
14 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/AddButton/index.scss:
--------------------------------------------------------------------------------
1 | .AddButton {
2 | position: relative;
3 |
4 | span.plus, span.reload {
5 | position: absolute;
6 | right: 4px;
7 | top: 4px;
8 | width: 16px;
9 | height: 16px;
10 | line-height: 13px;
11 | border: 1px solid #ccc;
12 | border-radius: 2px;
13 | background-image: linear-gradient(#fff, #efefef);
14 | text-align: center;
15 | font-weight: normal;
16 | color: #888;
17 |
18 | &:hover {
19 | background: #fff;
20 | }
21 |
22 | &:active {
23 | background: #efefef;
24 | }
25 | }
26 |
27 | span.reload {
28 | right: 24px;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Config/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import clone from 'lodash.clone'
5 |
6 | require('./index.scss')
7 |
8 | class Config extends React.Component {
9 | constructor(props) {
10 | super(props)
11 | this.writeableFields = [
12 | 'dbfilename',
13 | 'requirepass',
14 | 'masterauth',
15 | 'maxclients',
16 | 'appendonly',
17 | 'save',
18 | 'dir',
19 | 'client-output-buffer-limit',
20 | 'notify-keyspace-events',
21 | 'rdbcompression',
22 | 'repl-disable-tcp-nodelay',
23 | 'repl-diskless-sync',
24 | 'cluster-require-full-coverage',
25 | 'aof-rewrite-incremental-fsync',
26 | 'aof-load-truncated',
27 | 'slave-serve-stale-data',
28 | 'slave-read-only',
29 | 'activerehashing',
30 | 'stop-writes-on-bgsave-error',
31 | 'lazyfree-lazy-eviction',
32 | 'lazyfree-lazy-expire',
33 | 'lazyfree-lazy-server-del',
34 | 'slave-lazy-flush',
35 | 'tcp-keepalive',
36 | 'maxmemory-samples',
37 | 'timeout',
38 | 'auto-aof-rewrite-percentage',
39 | 'auto-aof-rewrite-min-size',
40 | 'hash-max-ziplist-entries',
41 | 'hash-max-ziplist-value',
42 | 'list-max-ziplist-entries',
43 | 'list-max-ziplist-value',
44 | 'list-max-ziplist-size',
45 | 'list-compress-depth',
46 | 'set-max-intset-entries',
47 | 'zset-max-ziplist-entries',
48 | 'zset-max-ziplist-value',
49 | 'hll-sparse-max-bytes',
50 | 'lua-time-limit',
51 | 'slowlog-log-slower-than',
52 | 'slowlog-max-len',
53 | 'latency-monitor-threshold',
54 | 'repl-ping-slave-period',
55 | 'repl-timeout',
56 | 'repl-backlog-ttl',
57 | 'repl-diskless-sync-delay',
58 | 'slave-priority',
59 | 'min-slaves-to-write',
60 | 'min-slaves-max-lag',
61 | 'cluster-node-timeout',
62 | 'cluster-migration-barrier',
63 | 'cluster-slave-validity-factor',
64 | 'hz',
65 | 'watchdog-period',
66 | 'maxmemory',
67 | 'repl-backlog-size',
68 | 'loglevel',
69 | 'maxmemory-policy',
70 | 'appendfsync'
71 | ]
72 | this.groups = [
73 | {
74 | name: 'General',
75 | configs: [
76 | {name: 'port', type: 'number'},
77 | {name: 'bind'},
78 | {name: 'unixsocket'},
79 | {name: 'unixsocketperm', type: 'number'},
80 | {name: 'daemonize', type: 'boolean'},
81 | {name: 'pidfile'},
82 | {name: 'tcp-backlog', type: 'number'},
83 | {name: 'tcp-keepalive', type: 'number'},
84 | {name: 'timeout', type: 'number'},
85 | {name: 'databases', type: 'number'}
86 | ]
87 | },
88 | {
89 | name: 'Logging',
90 | configs: [
91 | {name: 'loglevel', type: ['debug', 'verbose', 'notice', 'warning']},
92 | {name: 'logfile'},
93 | {name: 'syslog-enabled', type: 'boolean'},
94 | {name: 'syslog-ident'},
95 | {name: 'syslog-facility'}
96 | ]
97 | },
98 | {
99 | name: 'Snap Shotting',
100 | configs: [
101 | {name: 'dbfilename'},
102 | {name: 'dir'},
103 | {name: 'save'},
104 | {name: 'stop-writes-on-bgsave-error', type: 'boolean'},
105 | {name: 'rdbcompression', type: 'boolean'},
106 | {name: 'rdbchecksum', type: 'boolean'}
107 | ]
108 | },
109 | {
110 | name: 'Replication',
111 | configs: [
112 | {name: 'slaveof'},
113 | {name: 'masterauth'},
114 | {name: 'slave-serve-stale-data', type: 'boolean'},
115 | {name: 'slave-read-only', type: 'boolean'},
116 | {name: 'repl-diskless-sync', type: 'boolean'},
117 | {name: 'repl-diskless-sync-delay', type: 'number'},
118 | {name: 'repl-ping-slave-period', type: 'number'},
119 | {name: 'repl-timeout', type: 'number'},
120 | {name: 'repl-disable-tcp-nodelay', type: 'boolean'},
121 | {name: 'repl-backlog-size'},
122 | {name: 'repl-backlog-ttl', type: 'number'},
123 | {name: 'slave-priority', type: 'number'},
124 | {name: 'min-slaves-to-write', type: 'number'},
125 | {name: 'min-slaves-max-lag', type: 'number'}
126 | ]
127 | },
128 | {
129 | name: 'Security',
130 | configs: [
131 | {name: 'requirepass'},
132 | {name: 'rename-command'}
133 | ]
134 | },
135 | {
136 | name: 'Limits',
137 | configs: [
138 | {name: 'maxclients'},
139 | {name: 'maxmemory'},
140 | {name: 'maxmemory-policy', type: ['noeviction', 'volatile-lru', 'allkeys-lru', 'volatile-random', 'allkeys-random', 'volatile-ttl']},
141 | {name: 'maxmemory-samples', type: 'number'}
142 | ]
143 | },
144 | {
145 | name: 'Append Only Mode',
146 | configs: [
147 | {name: 'appendonly', type: 'boolean'},
148 | {name: 'appendfilename'},
149 | {name: 'appendfsync', type: ['everysec', 'always', 'no']},
150 | {name: 'no-appendfsync-on-rewrite', type: 'boolean'},
151 | {name: 'auto-aof-rewrite-percentage', type: 'number'},
152 | {name: 'auto-aof-rewrite-min-size'},
153 | {name: 'aof-load-truncated', type: 'number'}
154 | ]
155 | },
156 | {
157 | name: 'LUA Scripting',
158 | configs: [
159 | {name: 'lua-time-limit', type: 'number'}
160 | ]
161 | },
162 | {
163 | name: 'Cluster',
164 | configs: [
165 | {name: 'cluster-enabled', type: 'boolean'},
166 | {name: 'cluster-config-file'},
167 | {name: 'cluster-node-timeout', type: 'number'},
168 | {name: 'cluster-slave-validity-factor', type: 'nubmer'},
169 | {name: 'cluster-migration-barrier', type: 'number'},
170 | {name: 'cluster-require-full-coverage', type: 'boolean'}
171 | ]
172 | },
173 | {
174 | name: 'Slow Log',
175 | configs: [
176 | {name: 'slowlog-log-slower-than', type: 'number'},
177 | {name: 'slowlog-max-len', type: 'number'}
178 | ]
179 | },
180 | {
181 | name: 'Latency Monitor',
182 | configs: [
183 | {name: 'latency-monitor-threshold', type: 'number'}
184 | ]
185 | },
186 | {
187 | name: 'Event Notification',
188 | configs: [
189 | {name: 'notify-keyspace-events'}
190 | ]
191 | },
192 | {
193 | name: 'Advanced Config',
194 | configs: [
195 | {name: 'hash-max-ziplist-entries', type: 'number'},
196 | {name: 'hash-max-ziplist-value', type: 'number'},
197 | {name: 'list-max-ziplist-entries', type: 'number'},
198 | {name: 'list-max-ziplist-value', type: 'number'},
199 | {name: 'set-max-intset-entries', type: 'number'},
200 | {name: 'zset-max-ziplist-entries', type: 'number'},
201 | {name: 'zset-max-ziplist-value', type: 'number'},
202 | {name: 'hll-sparse-max-bytes', type: 'number'},
203 | {name: 'activerehashing', type: 'boolean'},
204 | {name: 'client-output-buffer-limit'},
205 | {name: 'hz', type: 'number'},
206 | {name: 'aof-rewrite-incremental-fsync', type: 'boolean'}
207 | ]
208 | }
209 | ]
210 | this.state = {
211 | groups: [],
212 | unsavedRewrites: {},
213 | unsavedConfigs: {}
214 | }
215 | this.load()
216 | }
217 |
218 | load() {
219 | this.props.redis.config('get', '*').then(config => {
220 | const configs = {}
221 |
222 | for (let i = 0; i < config.length - 1; i += 2) {
223 | configs[config[i]] = config[i + 1]
224 | }
225 |
226 | const groups = clone(this.groups, true).map(g => {
227 | g.configs = g.configs.map(c => {
228 | if (typeof configs[c.name] !== 'undefined') {
229 | c.value = configs[c.name]
230 | delete configs[c.name]
231 | }
232 | return c
233 | }).filter(c => typeof c.value !== 'undefined')
234 | return g
235 | }).filter(g => g.configs.length)
236 |
237 | if (Object.keys(configs).length) {
238 | groups.push({name: 'Other', configs: Object.keys(configs).map(key => {
239 | return {
240 | name: key,
241 | value: configs[key]
242 | }
243 | })})
244 | }
245 |
246 | this.setState({
247 | groups,
248 | unsavedConfigs: {},
249 | unsavedRewrites: {}
250 | })
251 | })
252 | }
253 |
254 | componentWillUnmount() {
255 | this.props.redis.removeAllListeners('select', this.onSelectBinded)
256 | }
257 |
258 | renderGroup(group) {
259 | return (
263 |
{group.name}
264 | { group.configs.map(this.renderConfig, this) }
265 | )
266 | }
267 |
268 | change({name, value}) {
269 | this.state.unsavedConfigs[name] = value
270 | this.state.unsavedRewrites[name] = value
271 | this.setState({
272 | groups: this.state.groups,
273 | unsavedConfigs: this.state.unsavedConfigs,
274 | unsavedRewrites: this.state.unsavedRewrites
275 | })
276 | }
277 |
278 | renderConfig(config) {
279 | let input
280 | const props = {readOnly: this.writeableFields.indexOf(config.name) === -1}
281 | props.disabled = props.readOnly
282 | if (config.type === 'boolean' &&
283 | (config.value === 'yes' || config.value === 'no')) {
284 | input = ( {
286 | config.value = e.target.checked ? 'yes' : 'no'
287 | this.change(config)
288 | }} {...props}
289 | />)
290 | } else if (config.type === 'number' && String(parseInt(config.value, 10)) === config.value) {
291 | input = ( {
293 | config.value = e.target.value
294 | this.change(config)
295 | }} {...props}
296 | />)
297 | } else if (Array.isArray(config.type) && config.type.indexOf(config.value) !== -1) {
298 | input = ( {
300 | config.value = e.target.value
301 | this.change(config)
302 | }} {...props}
303 | >
304 | {config.type.map(option => {option} )}
305 | )
306 | } else {
307 | input = ( {
309 | config.value = e.target.value
310 | this.change(config)
311 | }} {...props}
312 | />)
313 | }
314 | return (
318 |
{config.name}
319 | { input }
320 |
{config.description}
321 |
)
322 | }
323 |
324 | isChanged(rewrite) {
325 | return Object.keys(this.state[rewrite ? 'unsavedRewrites' : 'unsavedConfigs']).length > 0
326 | }
327 |
328 | handleReload() {
329 | if (this.isChanged()) {
330 | showModal({
331 | title: 'Reload config?',
332 | content: 'Are you sure you want to reload the config? Your changes will be lost if you reload the config.',
333 | button: 'Reload'
334 | }).then(() => {
335 | this.load()
336 | })
337 | } else {
338 | this.load()
339 | }
340 | }
341 |
342 | handleSave() {
343 | showModal({
344 | title: 'Save the changes',
345 | content: 'Are you sure you want to apply the changes and save the changes to the config file?',
346 | button: 'Save'
347 | }).then(() => {
348 | return this.handleApply(true)
349 | }).then(res => {
350 | return this.props.redis.config('rewrite')
351 | }).then(res => {
352 | this.setState({unsavedRewrites: {}})
353 | }).catch(err => {
354 | alert(err.message)
355 | })
356 | }
357 |
358 | handleApply(embed) {
359 | const confirm = embed ? Promise.resolve(1) : showModal({
360 | title: 'Apply the changes',
361 | 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.',
362 | button: 'Apply'
363 | })
364 | return confirm.then(() => {
365 | const pipeline = this.props.redis.pipeline()
366 | const unsavedConfigs = this.state.unsavedConfigs
367 | Object.keys(unsavedConfigs).forEach(config => {
368 | pipeline.config('set', config, unsavedConfigs[config])
369 | })
370 | return pipeline.exec()
371 | }).then(res => {
372 | this.setState({unsavedConfigs: {}})
373 | })
374 | }
375 |
376 | render() {
377 | return (
378 |
379 |
384 |
385 | Reload
390 | Save To Config File
396 | {
401 | this.handleApply()
402 | }}
403 | >Apply
404 |
405 |
406 |
)
407 | }
408 | }
409 |
410 | export default Config
411 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Config/index.scss:
--------------------------------------------------------------------------------
1 | .Config {
2 | overflow: auto;
3 | .wrapper {
4 | width: 430px;
5 | margin: 0 auto;
6 | .nt-form-row label {
7 | width: 170px;
8 | }
9 | }
10 |
11 | h3 {
12 | font-size: 20px;
13 | }
14 |
15 | .nt-button-group {
16 | margin: 20px 0;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Footer.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import humanFormat from 'human-format'
5 |
6 | const timeScale = new humanFormat.Scale({
7 | ms: 1,
8 | s: 1000,
9 | min: 60000,
10 | h: 3600000,
11 | d: 86400000
12 | })
13 |
14 | const initState = {
15 | ttl: null,
16 | encoding: null,
17 | size: null
18 | }
19 |
20 | class Footer extends React.PureComponent {
21 | state = initState
22 |
23 | init(keyName, keyType) {
24 | if (!keyType && keyType !== 'none') {
25 | this.setState(initState)
26 | return
27 | }
28 | const pipeline = this.props.redis.pipeline()
29 | pipeline.pttl(keyName)
30 | pipeline.object('ENCODING', keyName)
31 |
32 | let sizeUnit = 'Members'
33 | switch (keyType) {
34 | case 'string': pipeline.strlen(keyName); sizeUnit = 'Bytes'; break
35 | case 'hash': pipeline.hlen(keyName); break
36 | case 'list': pipeline.llen(keyName); break
37 | case 'set': pipeline.scard(keyName); break
38 | case 'zset': pipeline.zcard(keyName); break
39 | }
40 |
41 | pipeline.exec((err, [[err1, pttl], [err2, encoding], res3]) => {
42 | this.setState({
43 | encoding: encoding ? `Encoding: ${encoding}` : '',
44 | ttl: pttl >= 0 ? `TTL: ${humanFormat(pttl, {scale: timeScale}).replace(' ', '')}` : null,
45 | size: (res3 && res3[1]) ? `${sizeUnit}: ${res3[1]}` : null
46 | })
47 | })
48 | }
49 |
50 | componentDidMount() {
51 | this.init(this.props.keyName, this.props.keyType)
52 | }
53 |
54 | componentWillReceiveProps(nextProps) {
55 | if (nextProps.keyName !== this.props.keyName ||
56 | nextProps.keyType !== this.props.keyType ||
57 | nextProps.version !== this.props.version) {
58 | this.init(nextProps.keyName, nextProps.keyType)
59 | }
60 | }
61 |
62 | render() {
63 | const desc = ['size', 'encoding', 'ttl']
64 | .map(key => ({key, value: this.state[key]}))
65 | .filter(item => typeof item.value === 'string')
66 | return (
67 | {
68 | desc.map(({key, value}) => {value} )
72 | }
73 | )
74 | }
75 | }
76 |
77 | export default Footer
78 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/Editor/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import Codemirror from 'medis-react-codemirror'
6 | require('codemirror/mode/javascript/javascript')
7 | require('codemirror/addon/lint/json-lint')
8 | require('codemirror/addon/lint/lint')
9 | require('codemirror/addon/selection/active-line')
10 | require('codemirror/addon/edit/closebrackets')
11 | require('codemirror/addon/edit/matchbrackets')
12 | require('codemirror/addon/search/search')
13 | require('codemirror/addon/search/searchcursor')
14 | require('codemirror/addon/search/jump-to-line')
15 | require('codemirror/addon/dialog/dialog')
16 | require('codemirror/addon/dialog/dialog.css')
17 | import jsonlint from 'jsonlint'
18 | window.jsonlint = jsonlint.parser
19 | require('codemirror/lib/codemirror.css')
20 | require('codemirror/addon/lint/lint.css')
21 | const msgpack = require('msgpack5')()
22 |
23 | require('./index.scss')
24 |
25 | class Editor extends React.PureComponent {
26 | constructor() {
27 | super()
28 |
29 | this.resizeObserver = new ResizeObserver(() => {
30 | this.updateLayout()
31 | })
32 |
33 | this.state = {
34 | currentMode: '',
35 | wrapping: true,
36 | changed: false,
37 | modes: {
38 | raw: false,
39 | json: false,
40 | messagepack: false
41 | }
42 | }
43 | }
44 |
45 | updateLayout() {
46 | const {wrapSelector, codemirror} = this.refs
47 |
48 | const $this = $(ReactDOM.findDOMNode(this))
49 | if ($this.width() < 372) {
50 | $(ReactDOM.findDOMNode(wrapSelector)).hide()
51 | } else {
52 | $(ReactDOM.findDOMNode(wrapSelector)).show()
53 | }
54 | if (codemirror) {
55 | codemirror.getCodeMirror().refresh()
56 | }
57 | }
58 |
59 | componentDidMount() {
60 | this.init(this.props.buffer)
61 | this.resizeObserver.observe(ReactDOM.findDOMNode(this))
62 | }
63 |
64 | componentWillUnmount() {
65 | this.resizeObserver.disconnect()
66 | }
67 |
68 | componentWillReceiveProps(nextProps) {
69 | if (nextProps.buffer !== this.props.buffer) {
70 | this.init(nextProps.buffer)
71 | }
72 | }
73 |
74 | init(buffer) {
75 | if (!buffer) {
76 | this.setState({currentMode: '', changed: false})
77 | return
78 | }
79 | const content = buffer.toString()
80 | const modes = {}
81 | modes.raw = content
82 | modes.json = tryFormatJSON(content, true)
83 | modes.messagepack = modes.json ? false : tryFormatMessagepack(buffer, true)
84 | let currentMode = 'raw'
85 | if (modes.messagepack) {
86 | currentMode = 'messagepack'
87 | } else if (modes.json) {
88 | currentMode = 'json'
89 | }
90 | this.setState({modes, currentMode, changed: false}, () => {
91 | this.updateLayout()
92 | })
93 | }
94 |
95 | save() {
96 | let content = this.state.modes.raw
97 | if (this.state.currentMode === 'json') {
98 | content = tryFormatJSON(this.state.modes.json)
99 | if (!content) {
100 | alert('The json is invalid. Please check again.')
101 | return
102 | }
103 | } else if (this.state.currentMode === 'messagepack') {
104 | content = tryFormatMessagepack(this.state.modes.messagepack)
105 | if (!content) {
106 | alert('The json is invalid. Please check again.')
107 | return
108 | }
109 | content = msgpack.encode(JSON.parse(content))
110 | }
111 | this.props.onSave(content, err => {
112 | if (err) {
113 | alert(`Redis save failed: ${err.message}`)
114 | } else {
115 | this.init(typeof content === 'string' ? Buffer.from(content) : content)
116 | }
117 | })
118 | }
119 |
120 | updateContent(mode, content) {
121 | if (this.state.modes[mode] !== content) {
122 | this.state.modes[mode] = content
123 | this.setState({modes: this.state.modes, changed: true})
124 | }
125 | }
126 |
127 | updateMode(evt) {
128 | const newMode = evt.target.value
129 | this.setState({currentMode: newMode})
130 | }
131 |
132 | focus() {
133 | const codemirror = this.refs.codemirror
134 | if (codemirror) {
135 | const node = ReactDOM.findDOMNode(codemirror)
136 | if (node) {
137 | node.focus()
138 | }
139 | }
140 | }
141 |
142 | handleKeyDown(evt) {
143 | if (!evt.ctrlKey && evt.metaKey && evt.keyCode === 83) {
144 | this.save()
145 | evt.preventDefault()
146 | evt.stopPropagation()
147 | }
148 | }
149 |
150 | render() {
151 | let viewer
152 | if (this.state.currentMode === 'raw') {
153 | viewer = ( )
166 | } else if (this.state.currentMode === 'json') {
167 | viewer = ( )
188 | } else if (this.state.currentMode === 'messagepack') {
189 | viewer = ( )
210 | } else {
211 | viewer =
212 | }
213 | return (
218 | { viewer }
219 |
222 |
223 | this.setState({wrapping: evt.target.checked})}
227 | />
228 | Wrapping
229 |
230 |
235 | Raw
236 | JSON
237 | MessagePack
238 |
239 | Save Changes
244 |
245 |
)
246 | }
247 | }
248 |
249 | export default Editor
250 |
251 | function tryFormatJSON(jsonString, beautify) {
252 | try {
253 | const o = JSON.parse(jsonString)
254 | if (o && typeof o === 'object' && o !== null) {
255 | if (beautify) {
256 | return JSON.stringify(o, null, '\t')
257 | }
258 | return JSON.stringify(o)
259 | }
260 | } catch (e) { /**/ }
261 |
262 | return false
263 | }
264 |
265 | function tryFormatMessagepack(buffer, beautify) {
266 | try {
267 | let o
268 | if (typeof buffer === 'string') {
269 | o = JSON.parse(buffer)
270 | } else {
271 | o = msgpack.decode(buffer)
272 | }
273 | if (o && typeof o === 'object' && o !== null) {
274 | if (beautify) {
275 | return JSON.stringify(o, null, '\t')
276 | }
277 | return JSON.stringify(o)
278 | }
279 | } catch (e) { /**/ }
280 |
281 | return false
282 | }
283 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/Editor/index.scss:
--------------------------------------------------------------------------------
1 | .Editor {
2 | position: relative;
3 | min-width: 0;
4 | textarea {
5 | border: none;
6 | width: 100%;
7 | height: 100%;
8 | }
9 |
10 | .CodeMirror {
11 | height: auto;
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | font-family: Consolas, monospace;
16 | }
17 |
18 | .ReactCodeMirror {
19 | position: relative;
20 | flex: 1;
21 | display: flex;
22 | width: 100%;
23 | overflow-y: auto;
24 |
25 | &:before {
26 | content: '';
27 | position: absolute;
28 | left: 0;
29 | top: 0;
30 | width: 45px;
31 | z-index: 1;
32 | background: #f7f7f7;
33 | border-right: 1px solid #ddd;
34 | height: 100%;
35 | }
36 | }
37 |
38 | .operation-pannel {
39 | z-index: 99;
40 | }
41 |
42 | .mode-selector {
43 | position: absolute;
44 | bottom: 10px;
45 | right: 10px;
46 | }
47 |
48 | .wrap-selector {
49 | position: absolute;
50 | bottom: 5px;
51 | right: 120px;
52 | padding: 0 10px;
53 | span {
54 | margin-left: 4px;
55 | }
56 | }
57 |
58 | button {
59 | position: absolute;
60 | bottom: 10px;
61 | left: 54px;
62 | z-index: 99;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/HashContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from '.'
5 | import SplitPane from 'react-split-pane'
6 | import {Table, Column} from 'fixed-data-table-contextmenu'
7 | import Editor from './Editor'
8 | import AddButton from '../../../AddButton'
9 | import ContentEditable from '../../../ContentEditable'
10 | import ReactDOM from 'react-dom'
11 | import {clipboard, remote} from 'electron'
12 |
13 | class HashContent extends BaseContent {
14 | save(value, callback) {
15 | if (typeof this.state.selectedIndex === 'number') {
16 | const [key] = this.state.members[this.state.selectedIndex]
17 | this.state.members[this.state.selectedIndex][1] = Buffer.from(value)
18 | this.setState({members: this.state.members})
19 | this.props.redis.hset(this.state.keyName, key, value, (err, res) => {
20 | this.props.onKeyContentChange()
21 | callback(err, res)
22 | })
23 | } else {
24 | alert('Please wait for data been loaded before saving.')
25 | }
26 | }
27 |
28 | load(index) {
29 | if (!super.load(index)) {
30 | return
31 | }
32 | const count = Number(this.cursor) ? 10000 : 500
33 | this.props.redis.hscanBuffer(this.state.keyName, this.cursor, 'MATCH', '*', 'COUNT', count, (_, [cursor, result]) => {
34 | for (let i = 0; i < result.length - 1; i += 2) {
35 | this.state.members.push([result[i].toString(), result[i + 1]])
36 | }
37 | this.cursor = cursor
38 | this.setState({members: this.state.members}, () => {
39 | if (typeof this.state.selectedIndex !== 'number' && this.state.members.length) {
40 | this.handleSelect(null, 0)
41 | }
42 | this.loading = false
43 | if (this.state.members.length - 1 < this.maxRow && Number(cursor)) {
44 | this.load()
45 | }
46 | })
47 | })
48 | }
49 |
50 | handleSelect(evt, selectedIndex) {
51 | const item = this.state.members[selectedIndex]
52 | if (item) {
53 | this.setState({selectedIndex, content: item[1]})
54 | }
55 | }
56 |
57 | handleKeyDown(e) {
58 | if (typeof this.state.selectedIndex === 'number' && typeof this.state.editableIndex !== 'number') {
59 | if (e.keyCode === 8) {
60 | this.deleteSelectedMember()
61 | return false
62 | }
63 | if (e.keyCode === 38) {
64 | if (this.state.selectedIndex > 0) {
65 | this.handleSelect(null, this.state.selectedIndex - 1)
66 | }
67 | return false
68 | }
69 | if (e.keyCode === 40) {
70 | if (this.state.selectedIndex < this.state.members.length - 1) {
71 | this.handleSelect(null, this.state.selectedIndex + 1)
72 | }
73 | return false
74 | }
75 | }
76 | }
77 |
78 | deleteSelectedMember() {
79 | if (typeof this.state.selectedIndex !== 'number') {
80 | return
81 | }
82 | showModal({
83 | title: 'Delete selected item?',
84 | button: 'Delete',
85 | content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
86 | }).then(() => {
87 | const members = this.state.members
88 | const deleted = members.splice(this.state.selectedIndex, 1)
89 | if (deleted.length) {
90 | this.props.redis.hdel(this.state.keyName, deleted[0])
91 | if (this.state.selectedIndex >= members.length - 1) {
92 | this.state.selectedIndex -= 1
93 | }
94 | this.setState({members, length: this.state.length - 1}, () => {
95 | this.props.onKeyContentChange()
96 | this.handleSelect(null, this.state.selectedIndex)
97 | })
98 | }
99 | })
100 | }
101 |
102 | showContextMenu(e, row) {
103 | this.handleSelect(null, row)
104 |
105 | const menu = remote.Menu.buildFromTemplate([
106 | {
107 | label: 'Copy to Clipboard',
108 | click: () => {
109 | clipboard.writeText(this.state.members[row][0])
110 | }
111 | },
112 | {
113 | type: 'separator'
114 | },
115 | {
116 | label: 'Rename Key',
117 | click: () => {
118 | this.setState({editableIndex: row})
119 | }
120 | },
121 | {
122 | label: 'Delete',
123 | click: () => {
124 | this.deleteSelectedMember()
125 | }
126 | }
127 | ])
128 | menu.popup(remote.getCurrentWindow())
129 | }
130 |
131 | render() {
132 | return (
139 |
146 |
{
153 | this.handleSelect(evt, index)
154 | this.setState({editableIndex: index})
155 | }}
156 | width={this.props.contentBarWidth}
157 | height={this.props.height}
158 | headerHeight={24}
159 | >
160 | {
164 | showModal({
165 | button: 'Insert Member',
166 | form: {
167 | type: 'object',
168 | properties: {
169 | 'Key:': {
170 | type: 'string'
171 | }
172 | }
173 | }
174 | }).then(res => {
175 | const data = res['Key:']
176 | const value = 'New Member'
177 | this.props.redis.hsetnx(this.state.keyName, data, value).then(inserted => {
178 | if (!inserted) {
179 | alert('The field already exists')
180 | return
181 | }
182 | this.state.members.push([data, Buffer.from(value)])
183 | this.setState({
184 | members: this.state.members,
185 | length: this.state.length + 1
186 | }, () => {
187 | this.props.onKeyContentChange()
188 | this.handleSelect(null, this.state.members.length - 1)
189 | })
190 | })
191 | })
192 | }}
193 | />
194 | }
195 | width={this.props.contentBarWidth}
196 | cell={({rowIndex}) => {
197 | const member = this.state.members[rowIndex]
198 | if (!member) {
199 | this.load(rowIndex)
200 | return 'Loading...'
201 | }
202 | return ( {
206 | const members = this.state.members
207 | const member = members[rowIndex]
208 | const keyName = this.state.keyName
209 | const source = member[0]
210 | if (source !== target && target) {
211 | this.props.redis.hexists(keyName, target).then(exists => {
212 | if (exists) {
213 | return showModal({
214 | title: 'Overwrite the field?',
215 | button: 'Overwrite',
216 | content: `Field "${target}" already exists. Are you sure you want to overwrite this field?`
217 | }).then(() => {
218 | let found
219 | for (let i = 0; i < members.length; i++) {
220 | if (members[i][0] === target) {
221 | found = i
222 | break
223 | }
224 | }
225 | if (typeof found === 'number') {
226 | members.splice(found, 1)
227 | this.setState({length: this.state.length - 1})
228 | }
229 | })
230 | }
231 | }).then(() => {
232 | member[0] = target
233 | this.props.redis.multi()
234 | .hdel(keyName, source)
235 | .hset(keyName, target, member[1]).exec()
236 | this.setState({members})
237 | }).catch(() => {})
238 | }
239 | this.setState({editableIndex: null})
240 | ReactDOM.findDOMNode(this).focus()
241 | }}
242 | html={member[0]}
243 | />)
244 | }}
245 | />
246 |
247 |
248 |
252 | )
253 | }
254 | }
255 |
256 | export default HashContent
257 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/ListContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from '.'
5 | import SplitPane from 'react-split-pane'
6 | import {Table, Column} from 'fixed-data-table-contextmenu'
7 | import Editor from './Editor'
8 | import SortHeaderCell from './SortHeaderCell'
9 | import AddButton from '../../../AddButton'
10 | import {remote} from 'electron'
11 |
12 | class ListContent extends BaseContent {
13 | save(value, callback) {
14 | const {selectedIndex, keyName, desc} = this.state
15 | if (typeof selectedIndex === 'number') {
16 | const members = this.state.members.slice()
17 | members[selectedIndex] = value.toString()
18 | this.setState({members})
19 | this.props.redis.lset(keyName, desc ? -1 - selectedIndex : selectedIndex, value, (err, res) => {
20 | this.props.onKeyContentChange()
21 | callback(err, res)
22 | })
23 | } else {
24 | alert('Please wait for data been loaded before saving.')
25 | }
26 | }
27 |
28 | load(index) {
29 | if (!super.load(index)) {
30 | return
31 | }
32 |
33 | const {members, length, keyName, desc} = this.state
34 | let from = members.length
35 | let to = Math.min(from === 0 ? 200 : from + 1000, length - 1)
36 | if (to < from) {
37 | this.loading = false
38 | return
39 | }
40 | if (desc) {
41 | [from, to] = [-1 - to, -1 - from]
42 | }
43 |
44 | this.props.redis.lrange(keyName, from, to, (_, results) => {
45 | if (this.state.desc !== desc) {
46 | // TODO: use a counter instead to avoid
47 | // cancel multiple loading attempts.
48 | // LIST & ZSET
49 | this.loading = false
50 | return
51 | }
52 | if (desc) {
53 | results.reverse()
54 | }
55 | const diff = to - from + 1 - results.length
56 | this.setState({
57 | members: members.concat(results),
58 | length: length - diff
59 | }, () => {
60 | if (typeof this.state.selectedIndex !== 'number' && this.state.members.length) {
61 | this.handleSelect(null, 0)
62 | }
63 | this.loading = false
64 | if (this.state.members.length - 1 < this.maxRow && !diff) {
65 | this.load()
66 | }
67 | })
68 | })
69 | }
70 |
71 | handleSelect(_, selectedIndex) {
72 | if (typeof this.state.members[selectedIndex] === 'undefined') {
73 | this.setState({selectedIndex: null})
74 | } else {
75 | this.setState({selectedIndex})
76 | }
77 | }
78 |
79 | async deleteSelectedMember() {
80 | if (typeof this.state.selectedIndex !== 'number') {
81 | return
82 | }
83 | await showModal({
84 | title: 'Delete selected item?',
85 | button: 'Delete',
86 | content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
87 | })
88 | const {selectedIndex, desc, length, keyName} = this.state
89 | const members = this.state.members.slice()
90 | const deleted = members.splice(selectedIndex, 1)
91 | if (deleted.length) {
92 | this.props.redis.lremindex(keyName, desc ? -1 - selectedIndex : selectedIndex)
93 |
94 | const nextSelectedIndex = selectedIndex >= members.length - 1
95 | ? selectedIndex - 1
96 | : selectedIndex
97 | this.setState({members, length: length - 1}, () => {
98 | this.props.onKeyContentChange()
99 | this.handleSelect(null, nextSelectedIndex)
100 | })
101 | }
102 | }
103 |
104 | handleKeyDown(e) {
105 | if (typeof this.state.selectedIndex === 'number') {
106 | switch (e.keyCode) {
107 | case 8:
108 | this.deleteSelectedMember()
109 | return false
110 | case 38:
111 | if (this.state.selectedIndex > 0) {
112 | this.handleSelect(null, this.state.selectedIndex - 1)
113 | }
114 | return false
115 | case 40:
116 | if (this.state.selectedIndex < this.state.members.length - 1) {
117 | this.handleSelect(null, this.state.selectedIndex + 1)
118 | }
119 | return false
120 | }
121 | }
122 | }
123 |
124 | showContextMenu(e, row) {
125 | this.handleSelect(null, row)
126 |
127 | const menu = remote.Menu.buildFromTemplate([
128 | {
129 | label: 'Delete',
130 | click: () => {
131 | this.deleteSelectedMember()
132 | }
133 | }
134 | ])
135 | menu.popup(remote.getCurrentWindow())
136 | }
137 |
138 | renderEditor() {
139 | const content = this.state.members[this.state.selectedIndex]
140 | const buffer = typeof content === 'string'
141 | ? Buffer.from(content)
142 | : undefined
143 | return
147 | }
148 |
149 | renderIndexColumn() {
150 | return this.setState({
155 | desc,
156 | members: [],
157 | selectedIndex: null
158 | })}
159 | desc={this.state.desc}
160 | />
161 | }
162 | width={this.props.indexBarWidth}
163 | isResizable
164 | cell={({rowIndex}) => {
165 | return { this.state.desc ? this.state.length - 1 - rowIndex : rowIndex }
166 | }}
167 | />
168 | }
169 |
170 | renderValueColumn() {
171 | return {
175 | const res = await showModal({
176 | button: 'Insert Item',
177 | form: {
178 | type: 'object',
179 | properties: {
180 | 'Insert To:': {
181 | type: 'string',
182 | enum: ['head', 'tail']
183 | }
184 | }
185 | }
186 | })
187 | const insertToHead = res['Insert To:'] === 'head'
188 | const method = insertToHead ? 'lpush' : 'rpush'
189 | const data = 'New Item'
190 | await this.props.redis[method](this.state.keyName, data)
191 |
192 | const members = this.state.members.slice()
193 | members[insertToHead ? 'unshift' : 'push'](data)
194 | this.setState({
195 | members,
196 | length: this.state.length + 1
197 | }, () => {
198 | this.props.onKeyContentChange()
199 | if (insertToHead) {
200 | this.handleSelect(null, 0)
201 | }
202 | })
203 | }}
204 | />
205 | }
206 | width={this.props.contentBarWidth - this.props.indexBarWidth}
207 | cell={({rowIndex}) => {
208 | const data = this.state.members[rowIndex]
209 | if (typeof data === 'undefined') {
210 | this.load(rowIndex)
211 | return 'Loading...'
212 | }
213 | return {data}
214 | }}
215 | />
216 | }
217 |
218 | render() {
219 | return (
226 |
232 |
244 | {this.renderIndexColumn()}
245 | {this.renderValueColumn()}
246 |
247 |
248 | {this.renderEditor()}
249 | )
250 | }
251 | }
252 |
253 | export default ListContent
254 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/SetContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from '.'
5 | import SplitPane from 'react-split-pane'
6 | import {Table, Column} from 'fixed-data-table-contextmenu'
7 | import Editor from './Editor'
8 | import AddButton from '../../../AddButton'
9 | import {remote} from 'electron'
10 |
11 | require('./index.scss')
12 |
13 | class SetContent extends BaseContent {
14 | save(value, callback) {
15 | if (typeof this.state.selectedIndex === 'number') {
16 | const oldValue = this.state.members[this.state.selectedIndex]
17 |
18 | const key = this.state.keyName
19 | this.props.redis.sismember(key, value).then(exists => {
20 | if (exists) {
21 | callback(new Error('The value already exists in the set'))
22 | return
23 | }
24 | this.props.redis.multi().srem(key, oldValue).sadd(key, value).exec((err, res) => {
25 | if (!err) {
26 | this.state.members[this.state.selectedIndex] = value.toString()
27 | this.setState({members: this.state.members})
28 | }
29 | this.props.onKeyContentChange()
30 | callback(err, res)
31 | })
32 | })
33 | } else {
34 | alert('Please wait for data been loaded before saving.')
35 | }
36 | }
37 |
38 | load(index) {
39 | if (!super.load(index)) {
40 | return
41 | }
42 | const count = Number(this.cursor) ? 10000 : 500
43 | this.props.redis.sscan(this.state.keyName, this.cursor, 'COUNT', count, (_, [cursor, results]) => {
44 | this.cursor = cursor
45 | const length = Number(cursor) ? this.state.length : this.state.members.length + results.length
46 |
47 | this.setState({
48 | members: this.state.members.concat(results),
49 | length
50 | }, () => {
51 | if (typeof this.state.selectedIndex !== 'number' && this.state.members.length) {
52 | this.handleSelect(null, 0)
53 | }
54 | this.loading = false
55 | if (this.state.members.length - 1 < this.maxRow && Number(cursor)) {
56 | this.load()
57 | }
58 | })
59 | })
60 | }
61 |
62 | handleSelect(evt, selectedIndex) {
63 | const content = this.state.members[selectedIndex]
64 | if (typeof content !== 'undefined') {
65 | this.setState({selectedIndex, content})
66 | }
67 | }
68 |
69 | handleKeyDown(e) {
70 | if (typeof this.state.selectedIndex === 'number') {
71 | if (e.keyCode === 8) {
72 | this.deleteSelectedMember()
73 | return false
74 | }
75 | if (e.keyCode === 38) {
76 | if (this.state.selectedIndex > 0) {
77 | this.handleSelect(null, this.state.selectedIndex - 1)
78 | }
79 | return false
80 | }
81 | if (e.keyCode === 40) {
82 | if (this.state.selectedIndex < this.state.members.length - 1) {
83 | this.handleSelect(null, this.state.selectedIndex + 1)
84 | }
85 | return false
86 | }
87 | }
88 | }
89 |
90 | deleteSelectedMember() {
91 | if (typeof this.state.selectedIndex !== 'number') {
92 | return
93 | }
94 | showModal({
95 | title: 'Delete selected item?',
96 | button: 'Delete',
97 | content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
98 | }).then(() => {
99 | const members = this.state.members
100 | const deleted = members.splice(this.state.selectedIndex, 1)
101 | if (deleted.length) {
102 | this.props.redis.srem(this.state.keyName, deleted)
103 | if (this.state.selectedIndex >= members.length - 1) {
104 | this.state.selectedIndex -= 1
105 | }
106 | this.setState({members, length: this.state.length - 1}, () => {
107 | this.props.onKeyContentChange()
108 | this.handleSelect(null, this.state.selectedIndex)
109 | })
110 | }
111 | })
112 | }
113 |
114 | showContextMenu(e, row) {
115 | this.handleSelect(null, row)
116 |
117 | const menu = remote.Menu.buildFromTemplate([
118 | {
119 | label: 'Delete',
120 | click: () => {
121 | this.deleteSelectedMember()
122 | }
123 | }
124 | ])
125 | menu.popup(remote.getCurrentWindow())
126 | }
127 |
128 | render() {
129 | return (
137 |
143 |
153 | {
156 | const member = this.state.members[rowIndex]
157 | if (typeof member === 'undefined') {
158 | this.load(rowIndex)
159 | return 'Loading...'
160 | }
161 | return {member}
162 | }}
163 | header={
164 | {
166 | showModal({
167 | button: 'Insert Member',
168 | form: {
169 | type: 'object',
170 | properties: {
171 | 'Value:': {
172 | type: 'string'
173 | }
174 | }
175 | }
176 | }).then(res => {
177 | const data = res['Value:']
178 | return this.props.redis.sismember(this.state.keyName, data).then(exists => {
179 | if (exists) {
180 | const error = 'Member already exists'
181 | alert(error)
182 | throw new Error(error)
183 | }
184 | return data
185 | })
186 | }).then(data => {
187 | this.props.redis.sadd(this.state.keyName, data).then(() => {
188 | this.state.members.push(data)
189 | this.setState({
190 | members: this.state.members,
191 | length: this.state.length + 1
192 | }, () => {
193 | this.props.onKeyContentChange()
194 | this.handleSelect(null, this.state.members.length - 1)
195 | })
196 | })
197 | })
198 | }}
199 | />
200 | }
201 | />
202 |
203 |
204 |
208 | )
209 | }
210 | }
211 |
212 | export default SetContent
213 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/SortHeaderCell.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {memo} from 'react'
4 | import {Cell} from 'fixed-data-table-contextmenu'
5 |
6 | function SortHeaderCell({onOrderChange, desc, title}) {
7 | function handleOnClick(evt) {
8 | onOrderChange(!desc)
9 | evt.preventDefault()
10 | evt.stopPropagation()
11 | }
12 |
13 | return (
16 |
17 | {title}
18 | {
19 |
24 | }
25 |
26 | | )
27 | }
28 |
29 | export default memo(SortHeaderCell)
30 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/StringContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from '.'
5 | import Editor from './Editor'
6 |
7 | class StringContent extends BaseContent {
8 | init(keyName, keyType) {
9 | super.init(keyName, keyType)
10 | this.props.redis.getBuffer(keyName, (_, buffer) => {
11 | this.setState({buffer: buffer instanceof Buffer ? buffer : Buffer.alloc(0)})
12 | })
13 | }
14 |
15 | save(value, callback) {
16 | if (this.state.keyName) {
17 | this.props.redis.setKeepTTL(this.state.keyName, value, (err, res) => {
18 | this.props.onKeyContentChange()
19 | callback(err, res)
20 | })
21 | } else {
22 | alert('Please wait for data been loaded before saving.')
23 | }
24 | }
25 |
26 | create() {
27 | return this.props.redis.set(this.state.keyName, '')
28 | }
29 |
30 | render() {
31 | return ( )
35 | }
36 | }
37 |
38 | export default StringContent
39 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/ZSetContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from '.'
5 | import SplitPane from 'react-split-pane'
6 | import {Table, Column} from 'fixed-data-table-contextmenu'
7 | import Editor from './Editor'
8 | import SortHeaderCell from './SortHeaderCell'
9 | import AddButton from '../../../AddButton'
10 | import ContentEditable from '../../../ContentEditable'
11 | import ReactDOM from 'react-dom'
12 | import {clipboard, remote} from 'electron'
13 | import sortedIndexBy from 'lodash.sortedindexby'
14 |
15 | require('./index.scss')
16 |
17 | class ZSetContent extends BaseContent {
18 | save(value, callback) {
19 | const {selectedIndex, members, keyName} = this.state
20 | if (typeof selectedIndex === 'number') {
21 | const item = members[selectedIndex]
22 | const oldValue = item[0]
23 | item[0] = value.toString()
24 | this.setState({members})
25 | this.props.redis.multi()
26 | .zrem(keyName, oldValue)
27 | .zadd(keyName, item[1], value)
28 | .exec((err, res) => {
29 | this.props.onKeyContentChange()
30 | callback(err, res)
31 | })
32 | } else {
33 | alert('Please wait for data been loaded before saving.')
34 | }
35 | }
36 |
37 | load(index) {
38 | if (!super.load(index)) {
39 | return
40 | }
41 | const {members, desc, length, keyName} = this.state
42 | const from = members.length
43 | const to = Math.min(from === 0 ? 200 : from + 1000, length - 1)
44 |
45 | const commandName = desc ? 'zrevrange' : 'zrange'
46 | this.props.redis[commandName](keyName, from, to, 'WITHSCORES', (_, results) => {
47 | if (this.state.desc !== desc || this.state.members.length !== from) {
48 | this.loading = false
49 | return
50 | }
51 | const items = []
52 | for (let i = 0; i < results.length - 1; i += 2) {
53 | items.push([results[i], results[i + 1]])
54 | }
55 | const diff = to - from + 1 - items.length
56 | this.setState({
57 | members: members.concat(items),
58 | length: length - diff
59 | }, () => {
60 | const currentMembers = this.state.members
61 | if (typeof this.state.selectedIndex !== 'number' && currentMembers.length) {
62 | this.handleSelect(null, 0)
63 | }
64 | this.loading = false
65 | if (currentMembers.length - 1 < this.maxRow && !diff) {
66 | this.load()
67 | }
68 | })
69 | })
70 | }
71 |
72 | handleSelect(_, selectedIndex) {
73 | const item = this.state.members[selectedIndex]
74 | if (item) {
75 | this.setState({selectedIndex})
76 | }
77 | }
78 |
79 | handleKeyDown(e) {
80 | const {selectedIndex, editableIndex, members} = this.state
81 | if (typeof selectedIndex === 'number' && typeof editableIndex !== 'number') {
82 | switch (e.keyCode) {
83 | case 8:
84 | this.deleteSelectedMember()
85 | return false
86 | case 38:
87 | if (selectedIndex > 0) {
88 | this.handleSelect(null, selectedIndex - 1)
89 | }
90 | return false
91 | case 40:
92 | if (selectedIndex < members.length - 1) {
93 | this.handleSelect(null, selectedIndex + 1)
94 | }
95 | return false
96 | }
97 | }
98 | }
99 |
100 | deleteSelectedMember() {
101 | if (typeof this.state.selectedIndex !== 'number') {
102 | return
103 | }
104 | showModal({
105 | title: 'Delete selected item?',
106 | button: 'Delete',
107 | content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
108 | }).then(() => {
109 | const members = this.state.members
110 | const deleted = members.splice(this.state.selectedIndex, 1)
111 | if (deleted.length) {
112 | this.props.redis.zrem(this.state.keyName, deleted[0])
113 | const nextSelectedIndex = this.state.selectedIndex >= members.length - 1
114 | ? this.state.selectedIndex - 1
115 | : this.state.selectedIndex
116 | this.setState({members, length: this.state.length - 1}, () => {
117 | this.props.onKeyContentChange()
118 | this.handleSelect(null, nextSelectedIndex)
119 | })
120 | }
121 | })
122 | }
123 |
124 | showContextMenu(_, row) {
125 | this.handleSelect(null, row)
126 |
127 | const menu = remote.Menu.buildFromTemplate([
128 | {
129 | label: 'Copy Score to Clipboard',
130 | click: () => {
131 | clipboard.writeText(this.state.members[row][1])
132 | }
133 | },
134 | {
135 | type: 'separator'
136 | },
137 | {
138 | label: 'Edit Score',
139 | click: () => {
140 | this.setState({editableIndex: row})
141 | }
142 | },
143 | {
144 | label: 'Delete',
145 | click: () => {
146 | this.deleteSelectedMember()
147 | }
148 | }
149 | ])
150 | menu.popup(remote.getCurrentWindow())
151 | }
152 |
153 | renderTable() {
154 | return {
161 | this.handleSelect(evt, index)
162 | this.setState({editableIndex: index})
163 | }}
164 | isColumnResizing={false}
165 | onColumnResizeEndCallback={this.props.setSize.bind(null, 'score')}
166 | width={this.props.contentBarWidth}
167 | height={this.props.height}
168 | headerHeight={24}
169 | >
170 | {this.renderScoreColumn()}
171 | {this.renderMemberColumn()}
172 |
173 | }
174 |
175 | renderScoreColumn() {
176 | return this.setState({
181 | desc,
182 | members: [],
183 | selectedIndex: null
184 | })}
185 | desc={this.state.desc}
186 | />
187 | }
188 | width={this.props.scoreBarWidth}
189 | isResizable
190 | cell={({rowIndex}) => {
191 | const member = this.state.members[rowIndex]
192 | if (!member) {
193 | return ''
194 | }
195 | return ( {
199 | const members = this.state.members.slice()
200 |
201 | try {
202 | await this.props.redis.zadd(this.state.keyName, newScore, members[rowIndex][0])
203 | // Don't sort when changing scores
204 | members[rowIndex][1] = newScore
205 | this.setState({
206 | members,
207 | editableIndex: null
208 | })
209 | } catch (err) {
210 | alert(err.message)
211 | this.setState({
212 | editableIndex: null
213 | })
214 | }
215 | ReactDOM.findDOMNode(this.refs.table).focus()
216 | }}
217 | html={member[1]}
218 | />)
219 | }}
220 | />
221 | }
222 |
223 | renderMemberColumn() {
224 | return {
228 | const res = await showModal({
229 | button: 'Insert Member',
230 | form: {
231 | type: 'object',
232 | properties: {
233 | 'Value:': {
234 | type: 'string'
235 | },
236 | 'Score:': {
237 | type: 'number'
238 | }
239 | }
240 | }
241 | })
242 | const data = res['Value:']
243 | const score = res['Score:']
244 | const rank = await this.props.redis.zscore(this.state.keyName, data)
245 | if (rank !== null) {
246 | const error = 'Member already exists'
247 | alert(error)
248 | return
249 | }
250 | await this.props.redis.zadd(this.state.keyName, score, data)
251 | const members = this.state.members.slice()
252 | const newMember = [data, score]
253 | const index = sortedIndexBy(
254 | members,
255 | newMember,
256 | (member) => Number(member[1]) * (this.state.desc ? -1 : 1)
257 | )
258 | if (index < members.length - 1) {
259 | members.splice(index, 0, newMember)
260 | this.setState({
261 | members,
262 | length: this.state.length + 1
263 | }, () => {
264 | this.props.onKeyContentChange()
265 | this.handleSelect(null, index)
266 | })
267 | }
268 | alert('Added successfully')
269 | }}/>
270 | }
271 | width={this.props.contentBarWidth - this.props.scoreBarWidth}
272 | cell={({rowIndex}) => {
273 | const member = this.state.members[rowIndex]
274 | if (!member) {
275 | this.load(rowIndex)
276 | return 'Loading...'
277 | }
278 | return {member[0]}
279 | }}
280 | />
281 | }
282 |
283 | renderEditor() {
284 | const item = this.state.members[this.state.selectedIndex]
285 | const buffer = item
286 | ? Buffer.from(item[0])
287 | : undefined
288 | return
292 | }
293 |
294 | render() {
295 | return (
302 |
308 | {this.renderTable()}
309 |
310 | {this.renderEditor()}
311 | )
312 | }
313 | }
314 |
315 | export default ZSetContent
316 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 |
5 | require('./index.scss')
6 |
7 | const getDefaultState = function () {
8 | return {
9 | keyName: null,
10 | content: null,
11 | desc: false,
12 | length: 0,
13 | members: []
14 | }
15 | }
16 |
17 | class BaseContent extends React.Component {
18 | constructor() {
19 | super()
20 | this.state = getDefaultState()
21 | this.maxRow = 0
22 | this.cursor = 0
23 | this.randomClass = 'base-content-' + (Math.random() * 100000 | 0)
24 | }
25 |
26 | init(keyName, keyType) {
27 | if (!keyName || !keyType) {
28 | return
29 | }
30 | this.loading = false
31 | this.setState(getDefaultState())
32 |
33 | const {redis} = this.props
34 |
35 | const method = {
36 | string: 'strlen',
37 | list: 'llen',
38 | set: 'scard',
39 | zset: 'zcard',
40 | hash: 'hlen'
41 | }[keyType]
42 |
43 | redis[method](keyName).then(length => {
44 | this.setState({keyName, length: length || 0})
45 | })
46 | }
47 |
48 | load(index) {
49 | if (index > this.maxRow) {
50 | this.maxRow = index
51 | }
52 | if (this.loading) {
53 | return
54 | }
55 | this.loading = true
56 | return true
57 | }
58 |
59 | rowClassGetter(index) {
60 | const item = this.state.members[index]
61 | if (typeof item === 'undefined') {
62 | return 'type-list is-loading'
63 | }
64 | if (index === this.state.selectedIndex) {
65 | return 'type-list is-selected'
66 | }
67 | return 'type-list'
68 | }
69 |
70 | componentDidMount() {
71 | this.init(this.props.keyName, this.props.keyType)
72 | }
73 |
74 | componentDidUpdate() {
75 | if (typeof this.state.scrollToRow === 'number') {
76 | this.setState({scrollToRow: null})
77 | }
78 | }
79 |
80 | componentWillReceiveProps(nextProps) {
81 | if (nextProps.keyName !== this.props.keyName ||
82 | nextProps.keyType !== this.props.keyType) {
83 | this.init(nextProps.keyName, nextProps.keyType)
84 | }
85 | }
86 |
87 | componentWillUnmount() {
88 | this.setState = function () {}
89 | }
90 | }
91 |
92 | export default BaseContent
93 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/BaseContent/index.scss:
--------------------------------------------------------------------------------
1 | .BaseContent {
2 | flex: 1;
3 | position: relative;
4 |
5 | .type-list {
6 | .index-label {
7 | background: #ccc;
8 | margin: 4px 4px 0 0;
9 | font-family: Consolas, monospace !important;
10 | padding: 0 4px !important;
11 | height: 16px;
12 | font-size: 11px !important;
13 | line-height: 16px !important;
14 | display: block;
15 | text-align: center;
16 | color: #fff;
17 | white-space: nowrap;
18 | overflow: hidden;
19 | text-overflow: ellipsis;
20 | }
21 | }
22 |
23 | .SortHeaderCell {
24 | position: relative;
25 |
26 | a {
27 | display: block;
28 | }
29 |
30 | img {
31 | position: absolute;
32 | right: -15px;
33 | top: 5px;
34 | }
35 |
36 | &.is-asc img {
37 | transform: rotate(180deg);
38 | }
39 | }
40 | .base-content {
41 | margin-top: -1px;
42 | position: relative;
43 | overflow: hidden;
44 | &:focus {
45 | outline: 0;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {PureComponent} from 'react'
4 | import {connect} from 'react-redux'
5 | import {setSize} from 'Redux/actions'
6 | import StringContent from './BaseContent/StringContent'
7 | import ListContent from './BaseContent/ListContent'
8 | import SetContent from './BaseContent/SetContent'
9 | import HashContent from './BaseContent/HashContent'
10 | import ZSetContent from './BaseContent/ZSetContent'
11 |
12 | require('./index.scss')
13 |
14 | class KeyContent extends PureComponent {
15 | constructor() {
16 | super()
17 | this.state = {}
18 | }
19 |
20 | render() {
21 | const props = {key: this.props.keyName, ...this.props}
22 | let view
23 | switch (this.props.keyType) {
24 | case 'string': view = ; break
25 | case 'list': view = ; break
26 | case 'set': view = ; break
27 | case 'hash': view = ; break
28 | case 'zset': view = ; break
29 | case 'none':
30 | view = (
31 |
32 |
The key has been deleted
33 |
)
34 | break
35 | }
36 | return { view }
37 | }
38 | }
39 |
40 | function mapStateToProps(state) {
41 | return {
42 | contentBarWidth: state.sizes.get('contentBarWidth') || 200,
43 | scoreBarWidth: state.sizes.get('scoreBarWidth') || 60,
44 | indexBarWidth: state.sizes.get('indexBarWidth') || 60
45 | }
46 | }
47 |
48 | const mapDispatchToProps = {
49 | setSize
50 | }
51 |
52 | export default connect(mapStateToProps, mapDispatchToProps)(KeyContent)
53 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/KeyContent/index.scss:
--------------------------------------------------------------------------------
1 | .notfound {
2 | position: absolute;
3 | top: 50%;
4 | font-size: 22px;
5 | color: #ccc;
6 | text-align: center;
7 | transform: translateY(-50%);
8 | width: 100%;
9 |
10 | p {
11 | margin: 0;
12 | }
13 |
14 | .icon {
15 | font-size: 62px;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/TabBar/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {memo} from 'react'
4 | require('./index.scss')
5 |
6 | const TABS = ['Content', 'Terminal', 'Config']
7 |
8 | function renderTabIcon(tab) {
9 | switch (tab) {
10 | case 'Content':
11 | return
12 | case 'Terminal':
13 | return
14 | case 'Config':
15 | return
16 | }
17 | }
18 |
19 | function renderTab(tab, {activeTab, onSelectTab}) {
20 | return onSelectTab(tab)}
24 | >
25 | {renderTabIcon(tab)}
26 | {tab}
27 |
28 | }
29 |
30 | function Content(props) {
31 | return {TABS.map(tab => renderTab(tab, props))}
32 | }
33 |
34 | export default memo(Content)
35 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/TabBar/index.scss:
--------------------------------------------------------------------------------
1 | .TabBar {
2 | text-align: right;
3 | border-bottom: 1px solid #d3d3d3;
4 | .item {
5 | display: inline-block;
6 | padding: 12px;
7 |
8 | .icon {
9 | margin-right: 5px;
10 | }
11 |
12 | &.is-active {
13 | background: #dbdfe1;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Terminal/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import commands from 'redis-commands'
5 | import splitargs from 'redis-splitargs'
6 | import 'jquery.terminal'
7 |
8 | require('../../../../../../../../node_modules/jquery.terminal/css/jquery.terminal.css')
9 | require('./index.scss')
10 |
11 | class Terminal extends React.PureComponent {
12 | constructor() {
13 | super()
14 | this.onSelectBinded = this.onSelect.bind(this)
15 | }
16 |
17 | componentDidMount() {
18 | const {redis} = this.props
19 | redis.on('select', this.onSelectBinded)
20 | const terminal = this.terminal = $(this.refs.terminal).terminal((command, term) => {
21 | if (!command) {
22 | return
23 | }
24 | command = splitargs(command)
25 | const commandName = command[0] && command[0].toUpperCase()
26 | if (commandName === 'FLUSHALL' || commandName === 'FLUSHDB') {
27 | term.push(input => {
28 | if (input.match(/y|yes/i)) {
29 | this.execute(term, command)
30 | term.pop()
31 | } else if (input.match(/n|no/i)) {
32 | term.pop()
33 | }
34 | }, {
35 | prompt: '[[;#aac6e3;]Are you sure (y/n)? ]'
36 | })
37 | } else {
38 | this.execute(term, command)
39 | }
40 | }, {
41 | greetings: '',
42 | exit: false,
43 | completion(command, callback) {
44 | const commandName = command.split(' ')[0]
45 | const lower = commandName.toLowerCase()
46 | const isUppercase = commandName.toUpperCase() === commandName
47 | callback(
48 | commands.list
49 | .filter(item => item.indexOf(lower) === 0)
50 | .map(item => {
51 | const last = item.slice(commandName.length)
52 | return commandName + (isUppercase ? last.toUpperCase() : last)
53 | })
54 | )
55 | },
56 | name: this.props.connectionKey,
57 | height: '100%',
58 | width: '100%',
59 | outputLimit: 200,
60 | prompt: `[[;#fff;]redis> ]`,
61 | keydown(e) {
62 | if (!terminal.enabled()) {
63 | return true
64 | }
65 | if (e.ctrlKey || e.metaKey) {
66 | if (e.keyCode >= 48 && e.keyCode <= 57) {
67 | return true
68 | }
69 | if ([84, 87, 78, 82, 81].indexOf(e.keyCode) !== -1) {
70 | return true
71 | }
72 | }
73 | if (e.ctrlKey) {
74 | if (e.keyCode === 67) {
75 | if (terminal.level() > 1) {
76 | terminal.pop()
77 | if (terminal.paused()) {
78 | terminal.resume()
79 | }
80 | }
81 | return false
82 | }
83 | }
84 | }
85 | })
86 | }
87 |
88 | onSelect(db) {
89 | this.props.onDatabaseChange(db)
90 | }
91 |
92 | execute(term, args) {
93 | term.pause()
94 | const redis = this.props.redis
95 | if (args.length === 1 && args[0].toUpperCase() === 'MONITOR') {
96 | redis.monitor((_, monitor) => {
97 | term.echo('[[;#aac6e3;]Enter monitor mode. Press Ctrl+C to exit. ]')
98 | term.resume()
99 | term.push(input => {
100 | }, {
101 | onExit() {
102 | monitor.disconnect()
103 | }
104 | })
105 | monitor.on('monitor', (time, args) => {
106 | if (term.level() > 1) {
107 | term.echo(formatMonitor(time, args), {raw: true})
108 | }
109 | })
110 | })
111 | } else if (args.length > 1 && ['SUBSCRIBE', 'PSUBSCRIBE'].indexOf(args[0].toUpperCase()) !== -1) {
112 | const newRedis = redis.duplicate()
113 | newRedis.call.apply(newRedis, args).then(res => {
114 | term.echo('[[;#aac6e3;]Enter subscription mode. Press Ctrl+C to exit. ]')
115 | term.resume()
116 | term.push(input => {
117 | }, {
118 | prompt: '',
119 | onExit() {
120 | newRedis.disconnect()
121 | }
122 | })
123 | })
124 | newRedis.on('message', (channel, message) => {
125 | term.echo(formatMessage(channel, message), {raw: true})
126 | })
127 | newRedis.on('pmessage', (pattern, channel, message) => {
128 | term.echo(formatMessage(channel, message), {raw: true})
129 | })
130 | } else {
131 | redis.call.apply(redis, args).then(res => {
132 | term.echo(getHTML(res), {raw: true})
133 | term.resume()
134 | }).catch(err => {
135 | term.echo(getHTML(err), {raw: true})
136 | term.resume()
137 | })
138 | }
139 | }
140 |
141 | componentWillReceiveProps(nextProps) {
142 | if (this.props.style.display === 'none' && nextProps.style.display === 'block') {
143 | this.terminal.focus()
144 | }
145 | }
146 |
147 | componentWillUnmount() {
148 | this.props.redis.removeAllListeners('select', this.onSelectBinded)
149 | }
150 |
151 | render() {
152 | return (
)
153 | }
154 | }
155 |
156 | export default Terminal
157 |
158 | function getHTML(response) {
159 | if (Array.isArray(response)) {
160 | return `
161 | ${response.map((item, index) => '' + index + ' ' + getHTML(item) + ' ').join('')}
162 | `
163 | }
164 | const type = typeof response
165 | if (type === 'number') {
166 | return `${response}
`
167 | }
168 | if (type === 'string') {
169 | return `${response.replace(/\r?\n/g, ' ')}
`
170 | }
171 | if (response === null) {
172 | return `null
`
173 | }
174 | if (response instanceof Error) {
175 | return `${response.message}
`
176 | }
177 | if (type === 'object') {
178 | return `
179 | ${Object.keys(response).map(item => '' + item + ' ' + getHTML(response[item]) + ' ').join('')}
180 | `
181 | }
182 |
183 | return `${JSON.stringify(response)}
`
184 | }
185 |
186 | function formatMonitor(time, args) {
187 | args = args || []
188 | const command = args[0] ? args.shift().toUpperCase() : ''
189 | if (command) {
190 | commands.getKeyIndexes(command.toLowerCase(), args).forEach(index => {
191 | args[index] = `${args[index]} `
192 | })
193 | }
194 | return `
195 | ${time}
196 |
197 | ${command}
198 | ${args.join(' ')}
199 |
200 |
`
201 | }
202 |
203 | function formatMessage(channel, message) {
204 | return `
205 | ${channel}
206 | ${message}
207 |
`
208 | }
209 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/Terminal/index.scss:
--------------------------------------------------------------------------------
1 | .Terminal {
2 | overflow: auto;
3 | flex: 1;
4 | * {
5 | -webkit-user-select: text;
6 | }
7 |
8 | &.terminal, .cmd {
9 | --background: #272b34;
10 | --background: #000;
11 | --size: 1.4;
12 | font-family: Consolas, monospace;
13 | }
14 |
15 | .number {
16 | color: #78CF8A;
17 | }
18 |
19 | .string {
20 | color: #d6ec9c;
21 | }
22 |
23 | .array-resp, .object-resp {
24 | margin: 0;
25 | padding: 0;
26 |
27 | li {
28 | display: flex;
29 | span {
30 | color: #848080;
31 | min-width: 28px;
32 | text-align: right;
33 | margin-right: 10px;
34 | }
35 | div {
36 | flex: 1;
37 | }
38 | }
39 | }
40 |
41 | .object-resp li span {
42 | font-weight: bold;
43 | color: #cda869;
44 | }
45 |
46 | .null {
47 | color: #cf7ea9;
48 | }
49 |
50 | .error {
51 | color: #ee6868 !important;
52 | }
53 |
54 | .list {
55 | color: #8f9d6a;
56 | }
57 |
58 | .monitor {
59 | color: #8f9d6a;
60 | .time {
61 | color: #3d3d3d;
62 | margin-right: 4px;
63 | }
64 |
65 | .command-name {
66 | color: #cf7ea9;
67 | }
68 |
69 | .command-key {
70 | color: #cda869;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/Content/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import TabBar from './TabBar'
5 | import KeyContent from './KeyContent'
6 | import Terminal from './Terminal'
7 | import Config from './Config'
8 | import Footer from './Footer'
9 |
10 | class Content extends React.PureComponent {
11 | constructor() {
12 | super()
13 | this.state = {
14 | pattern: '',
15 | db: 0,
16 | version: 0,
17 | tab: 'Content'
18 | }
19 | }
20 |
21 | init(keyName) {
22 | this.setState({keyType: null})
23 | if (keyName !== null) {
24 | this.setState({keyType: null})
25 | this.props.redis.type(keyName).then(keyType => {
26 | if (keyName === this.props.keyName) {
27 | this.setState({keyType})
28 | }
29 | })
30 | }
31 | }
32 |
33 | componentDidMount() {
34 | this.init(this.props.keyName)
35 | }
36 |
37 | componentWillReceiveProps(nextProps) {
38 | if (nextProps.keyName !== this.props.keyName || nextProps.version !== this.props.version) {
39 | this.init(nextProps.keyName)
40 | }
41 | if (nextProps.metaVersion !== this.props.metaVersion) {
42 | this.setState({version: this.state.version + 1})
43 | }
44 | }
45 |
46 | handleTabChange(tab) {
47 | this.setState({tab})
48 | }
49 |
50 | render() {
51 | return (
52 |
56 | {
63 | this.setState({version: this.state.version + 1})
64 | }}
65 | />
66 |
73 |
79 |
85 |
)
86 | }
87 | }
88 |
89 | export default Content
90 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/ContentEditable/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import escape from 'lodash.escape'
4 |
5 | require('./index.scss')
6 |
7 | export default class ContentEditable extends React.Component {
8 | constructor() {
9 | super()
10 | }
11 |
12 | render() {
13 | const {html, enabled, ...props} = this.props
14 | return (
17 |
25 |
)
26 | }
27 |
28 | shouldComponentUpdate(nextProps) {
29 | return nextProps.html !== this.props.html || // ReactDOM.findDOMNode(this.refs.text).innerHTML ||
30 | nextProps.enabled !== this.props.enabled
31 | }
32 |
33 | componentDidMount() {
34 | if (this.props.enabled) {
35 | ReactDOM.findDOMNode(this.refs.text).focus()
36 | }
37 | }
38 |
39 | componentDidUpdate() {
40 | const node = ReactDOM.findDOMNode(this.refs.text)
41 | if (escape(this.props.html) !== node.innerHTML) {
42 | node.innerHTML = this.props.html
43 | }
44 | if (this.props.enabled) {
45 | const range = document.createRange()
46 | range.selectNodeContents(node)
47 | const sel = window.getSelection()
48 | sel.removeAllRanges()
49 | sel.addRange(range)
50 | }
51 | }
52 |
53 | handleKeyDown(evt) {
54 | if (evt.keyCode === 13) {
55 | ReactDOM.findDOMNode(this.refs.text).blur()
56 | evt.preventDefault()
57 | evt.stopPropagation()
58 | return
59 | }
60 | if (evt.keyCode === 27) {
61 | this.props.onChange(this.props.html)
62 | evt.preventDefault()
63 | evt.stopPropagation()
64 | }
65 | }
66 |
67 | handleChange(evt) {
68 | const html = ReactDOM.findDOMNode(this.refs.text).innerHTML
69 | if (html !== this.lastHtml) {
70 | evt.target = {value: html}
71 | }
72 | this.lastHtml = html
73 | }
74 |
75 | handleSubmit() {
76 | this.props.onChange(ReactDOM.findDOMNode(this.refs.text).textContent)
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/ContentEditable/index.scss:
--------------------------------------------------------------------------------
1 | .ContentEditable {
2 | [contenteditable="true"] {
3 | background: #fff !important;
4 | color: #333;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/Footer.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 |
5 | class Footer extends React.Component {
6 | constructor() {
7 | super()
8 | this.state = {}
9 | }
10 |
11 | componentDidMount() {
12 | this.updateInfo()
13 | this.updateDBCount()
14 | this.interval = setInterval(this.updateInfo.bind(this), 10000)
15 | }
16 |
17 | componentWillReceiveProps(nextProps) {
18 | if (nextProps.db !== this.props.db) {
19 | this.updateInfo()
20 | }
21 | }
22 |
23 | updateDBCount() {
24 | this.props.redis.config('get', 'databases', (err, res) => {
25 | if (!err && res[1]) {
26 | this.setState({databases: Number(res[1])})
27 | } else {
28 | const redis = this.props.redis.duplicate()
29 | const select = redis.select.bind(redis)
30 | this.guessDatabaseNumber(select, 15).then(count => {
31 | return typeof count === 'number' ? count : this.guessDatabaseNumber(select, 1, 0)
32 | }).then(count => {
33 | this.setState({databases: count + 1})
34 | })
35 | }
36 | })
37 | }
38 |
39 | updateInfo() {
40 | this.props.redis.info((err, res) => {
41 | if (err) {
42 | return
43 | }
44 | const info = {}
45 |
46 | const lines = res.split('\r\n')
47 | for (let i = 0; i < lines.length; i++) {
48 | const parts = lines[i].split(':')
49 | if (parts[1]) {
50 | info[parts[0]] = parts[1]
51 | }
52 | }
53 |
54 | this.setState(info)
55 | })
56 | }
57 |
58 | guessDatabaseNumber(select, startIndex, lastSuccessIndex) {
59 | if (startIndex > 30) {
60 | return Promise.resolve(30)
61 | }
62 | return select(startIndex)
63 | .then(() => {
64 | return this.guessDatabaseNumber(select, startIndex + 1, startIndex)
65 | }).catch(err => {
66 | if (typeof lastSuccessIndex === 'number') {
67 | return lastSuccessIndex
68 | }
69 | return null
70 | })
71 | }
72 |
73 | componentWillUnmount() {
74 | clearInterval(this.interval)
75 | this.interval = null
76 | }
77 |
78 | handleChange(evt) {
79 | const db = Number(evt.target.value)
80 | this.props.onDatabaseChange(db)
81 | }
82 |
83 | keyCountByDb(dbNumber){
84 | const db = `db${dbNumber}`
85 | let keys = 0
86 | if (this.state[db]) {
87 | const match = this.state[db].match(/keys=(\d+)/)
88 | if (match) {
89 | keys = match[1]
90 | }
91 | }
92 | return keys
93 | }
94 |
95 | render() {
96 | const keys = this.keyCountByDb(this.props.db)
97 | return (
98 | Keys: {keys}
99 |
100 | DB:
101 |
112 | {(max => {
113 | return new Array(max).fill(0).map((value, db) => {
114 | return (
115 |
116 | {db} {this.keyCountByDb(db) > 0 ? `(${this.keyCountByDb(db)})` : ''}
117 |
118 | );
119 | });
120 | })(this.state.databases || 1)}
121 |
122 |
123 | )
124 | }
125 | }
126 |
127 | export default Footer
128 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/KeyList/index.scss:
--------------------------------------------------------------------------------
1 | .pattern-table {
2 | position: relative;
3 | overflow: hidden;
4 | &:focus {
5 | outline: 0;
6 | }
7 |
8 | footer {
9 | height: 24px;
10 | }
11 | }
12 |
13 | .public_fixedDataTable_bottomShadow {
14 | display: none;
15 | }
16 |
17 | .key-type {
18 | margin: 4px 0 0;
19 | padding: 0 !important;
20 | text-transform: uppercase;
21 | width: 32px;
22 | height: 16px;
23 | font-size: 11px !important;
24 | line-height: 17px !important;
25 | display: block;
26 | text-align: center;
27 | background: #60d4ca;
28 | color: #fff;
29 |
30 | &.str { background: #5dc936; }
31 | &.list { background: #fca32a; }
32 | &.hash { background: #b865d0; }
33 | &.zset { background: #fa5049; }
34 | &.set { background: #239ff2; }
35 | }
36 |
37 | .public_fixedDataTable_header, .public_fixedDataTableRow_main.is-loading {
38 | .public_fixedDataTableCell_main {
39 | font-family: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif !important;
40 | }
41 | }
42 |
43 | .public_fixedDataTableCell_cellContent {
44 | padding: 0;
45 | }
46 |
47 | .public_fixedDataTableCell_main {
48 | font-family: Consolas, monospace;
49 | font-size: 12px;
50 | line-height: 24px;
51 | padding: 0 8px;
52 | }
53 |
54 | .public_fixedDataTableRow_main {
55 | color: #606061;
56 | }
57 |
58 | :focus .public_fixedDataTableRow_main.is-selected {
59 | background: #116cd6;
60 | color: #fff;
61 |
62 | .public_fixedDataTableCell_main {
63 | background: transparent;
64 | }
65 | }
66 |
67 | .public_fixedDataTableRow_main.is-selected {
68 | background: #dcdcdc;
69 |
70 | .public_fixedDataTableCell_main {
71 | background: transparent;
72 | }
73 | }
74 |
75 | .public_fixedDataTableCell_main {
76 | border: none;
77 | }
78 |
79 | .public_fixedDataTable_main {
80 | border-right: none;
81 | border-left: none;
82 | border-bottom: none;
83 | }
84 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/PatternList/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import {ipcRenderer} from 'electron'
5 |
6 | require('./index.scss')
7 |
8 | class PatternList extends React.Component {
9 | constructor(props) {
10 | super()
11 | this.state = {
12 | patternDropdown: false,
13 | pattern: props.pattern
14 | }
15 | }
16 |
17 | componentWillReceiveProps(nextProps) {
18 | if (nextProps.db !== this.props.db) {
19 | this.updatePattern('')
20 | }
21 | if (nextProps.pattern !== this.props.pattern) {
22 | this.setState({pattern: nextProps.pattern})
23 | }
24 | }
25 |
26 | updatePattern(value) {
27 | this.setState({pattern: value})
28 | this.props.onChange(value)
29 | }
30 |
31 | render() {
32 | return (
33 |
34 |
{
40 | this.updatePattern(evt.target.value)
41 | }}
42 | />
43 |
{
46 | this.setState({patternDropdown: !this.state.patternDropdown})
47 | }}
48 | />
49 |
53 |
54 | {
55 | this.props.patterns.map(pattern => {
56 | return ( {
58 | const value = pattern.get('value')
59 | this.props.onChange(value)
60 | this.setState({patternDropdown: false, pattern: value})
61 | }}
62 | >{pattern.get('name')} )
63 | })
64 | }
65 | {
68 | ipcRenderer.send('create patternManager', `${this.props.connectionKey}|${this.props.db}`)
69 | }}
70 | >
71 |
72 | Manage Patterns...
73 |
74 |
75 |
76 | )
77 | }
78 | }
79 |
80 | export default PatternList
81 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/PatternList/index.scss:
--------------------------------------------------------------------------------
1 | .pattern-input {
2 | position: relative;
3 | padding: 6px;
4 | .icon-search {
5 | position: absolute;
6 | left: 14px;
7 | top: 12px;
8 | opacity: 0.5;
9 | }
10 | .icon-down-open {
11 | position: absolute;
12 | right: 0;
13 | top: 0;
14 | opacity: 0.5;
15 | transition: 0.1s;
16 | display: inline-block;
17 | width: 40px;
18 | text-align: center;
19 | height: 42px;
20 | line-height: 42px;
21 |
22 | &.is-active {
23 | transform: rotate(180deg);
24 | }
25 | }
26 |
27 | input {
28 | padding-left: 22px;
29 | }
30 | }
31 |
32 | .pattern-dropdown {
33 | position: absolute;
34 | z-index: 999;
35 | background: #fff;
36 | margin-top: 6px;
37 | left: 0;
38 | width: 100%;
39 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.21);
40 | transition: transform 125ms cubic-bezier(0.18, 0.89, 0.32, 1.12), opacity 100ms linear;
41 | transform-origin: top;
42 | transform: scale(1, 0.2);
43 | pointer-events: none;
44 | opacity: 0;
45 |
46 | &.is-active {
47 | pointer-events: initial;
48 | opacity: 1;
49 | transform: scale(1, 1);
50 | }
51 |
52 | ul {
53 | height: 100%;
54 | overflow: auto;
55 | }
56 |
57 | li {
58 | display: block;
59 | padding: 8px 12px;
60 | font-family: Consolas, monospace;
61 | border-top: 1px solid #f5f5f4;
62 |
63 | &:hover {
64 | background: #116cd6;
65 | color: #fff;
66 | }
67 |
68 | &:last-child {
69 | font-family: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif;
70 | }
71 | }
72 | }
73 | .manage-pattern-button {
74 | color: #116cd6;
75 |
76 | span.icon {
77 | margin-right: 5px;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/KeyBrowser/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {memo} from 'react'
4 | import {List} from 'immutable'
5 | import PatternList from './PatternList'
6 | import KeyList from './KeyList'
7 | import Footer from './Footer'
8 |
9 | const FOOTER_HEIGHT = 66
10 |
11 | function KeyBrowser({
12 | pattern, patterns, connectionKey, db, height, width, redis,
13 | onPatternChange, onCreateKey, onKeyMetaChange, onSelectKey, onDatabaseChange
14 | }) {
15 | const clientHeight = height - FOOTER_HEIGHT
16 | return ()
37 | }
38 |
39 | export default memo(KeyBrowser)
40 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import {connect} from 'react-redux'
5 | import SplitPane from 'react-split-pane'
6 | import KeyBrowser from './KeyBrowser'
7 | import Content from './Content'
8 | require('./index.scss')
9 |
10 | class Database extends React.PureComponent {
11 | constructor() {
12 | super()
13 | this.$window = $(window)
14 |
15 | this.state = {
16 | sidebarWidth: 260,
17 | key: null,
18 | db: 0,
19 | version: 0,
20 | metaVersion: 0,
21 | pattern: '',
22 | clientHeight: this.$window.height() - $('#tabGroupWrapper').height()
23 | }
24 | }
25 |
26 | componentDidMount() {
27 | this.updateLayoutBinded = this.updateLayout.bind(this)
28 | $(window).on('resize', this.updateLayoutBinded)
29 | this.updateLayout()
30 | }
31 |
32 | componentWillUnmount() {
33 | $(window).off('resize', this.updateLayoutBinded)
34 | }
35 |
36 | updateLayout() {
37 | this.setState({
38 | clientHeight: this.$window.height() - $('#tabGroupWrapper').height()
39 | })
40 | }
41 |
42 | handleCreateKey(key) {
43 | this.setState({key, pattern: key})
44 | }
45 |
46 | render() {
47 | return ( {
54 | this.setState({sidebarWidth: size})
55 | }}
56 | >
57 | this.setState({pattern})}
61 | height={this.state.clientHeight}
62 | width={this.state.sidebarWidth}
63 | redis={this.props.redis}
64 | connectionKey={this.props.connectionKey}
65 | onSelectKey={key => this.setState({key, version: this.state.version + 1})}
66 | onCreateKey={this.handleCreateKey.bind(this)}
67 | db={this.state.db}
68 | onDatabaseChange={db => this.setState({db})}
69 | onKeyMetaChange={() => this.setState({metaVersion: this.state.metaVersion + 1})}
70 | />
71 | this.setState({db})}
80 | />
81 | )
82 | }
83 | }
84 |
85 | function mapStateToProps(state, {instance}) {
86 | return {
87 | patterns: state.patterns,
88 | redis: instance.get('redis'),
89 | connectionKey: instance.get('connectionKey')
90 | }
91 | }
92 |
93 | export default connect(mapStateToProps)(Database)
94 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/DatabaseContainer/index.scss:
--------------------------------------------------------------------------------
1 | .Resizer {
2 | background: #000;
3 | opacity: .2;
4 | z-index: 1;
5 | -moz-box-sizing: border-box;
6 | -webkit-box-sizing: border-box;
7 | box-sizing: border-box;
8 | -moz-background-clip: padding;
9 | -webkit-background-clip: padding;
10 | background-clip: padding-box;
11 | }
12 |
13 | .Resizer.horizontal {
14 | height: 11px;
15 | margin: -5px 0;
16 | border-top: 5px solid rgba(255, 255, 255, 0);
17 | border-bottom: 5px solid rgba(255, 255, 255, 0);
18 | cursor: row-resize;
19 | width: 100%;
20 | }
21 |
22 | .Resizer.vertical {
23 | width: 11px;
24 | margin: 0 -5px;
25 | border-left: 5px solid rgba(255, 255, 255, 0);
26 | border-right: 5px solid rgba(255, 255, 255, 0);
27 | cursor: col-resize;
28 | height: 100%;
29 | }
30 |
31 | .overflow-wrapper {
32 | display: flex;
33 | width: calc(100% + 16px);
34 | margin-left: -8px;
35 |
36 | span {
37 | padding: 0 8px;
38 | display: block;
39 | flex: 1;
40 | width: 100%;
41 | white-space: nowrap;
42 | overflow: hidden;
43 | text-overflow: ellipsis;
44 | }
45 |
46 | span[contenteditable="true"] {
47 | text-overflow: clip;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/Modal/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/src/renderer/windows/MainWindow/InstanceContent/Modal/icon.png
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/Modal/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | require('json-editor')
4 |
5 | require('./index.scss')
6 |
7 | export default class Modal extends React.Component {
8 | handleSubmit() {
9 | if (this.editor) {
10 | const errors = this.editor.validate()
11 | if (errors.length) {
12 | $('.ui-state-error', ReactDOM.findDOMNode(this.refs.form)).css('opacity', 1)
13 | return
14 | }
15 | this.props.onSubmit(this.editor.getValue())
16 | } else {
17 | this.props.onSubmit(1)
18 | }
19 | }
20 |
21 | handleCancel() {
22 | this.props.onCancel()
23 | }
24 |
25 | componentDidMount() {
26 | if (this.props.form) {
27 | this.editor = new JSONEditor(ReactDOM.findDOMNode(this.refs.form), {
28 | disable_array_add: true,
29 | disable_array_delete: true,
30 | disable_array_reorder: true,
31 | disable_collapse: true,
32 | disable_edit_json: true,
33 | disable_properties: true,
34 | required_by_default: true,
35 | schema: this.props.form,
36 | show_errors: 'always',
37 | theme: 'jqueryui'
38 | })
39 |
40 | $('.row input, .row select', ReactDOM.findDOMNode(this.refs.form)).first().focus()
41 | } else {
42 | $('.nt-button', ReactDOM.findDOMNode(this)).first().focus()
43 | }
44 | }
45 |
46 | handleKeyDown(evt) {
47 | if (evt.keyCode === 9) {
48 | const $all = $('.row input, .row select, .nt-button', ReactDOM.findDOMNode(this))
49 | const focused = $(':focus')[0]
50 | let i
51 | for (i = 0; i < $all.length - 1; ++i) {
52 | if ($all[i] != focused) {
53 | continue
54 | }
55 | $all[i + 1].focus()
56 | $($all[i + 1]).select()
57 | break
58 | }
59 | // Must have been focused on the last one or none of them.
60 | if (i == $all.length - 1) {
61 | $all[0].focus()
62 | $($all[0]).select()
63 | }
64 | evt.stopPropagation()
65 | evt.preventDefault()
66 | return
67 | }
68 | if (evt.keyCode === 27) {
69 | this.handleCancel()
70 | evt.stopPropagation()
71 | evt.preventDefault()
72 | return
73 | }
74 | if (evt.keyCode === 13) {
75 | const node = ReactDOM.findDOMNode(this.props.form ? this.refs.cancel : this.refs.submit)
76 | node.focus()
77 | setTimeout(() => {
78 | node.click()
79 | }, 10)
80 | evt.stopPropagation()
81 | evt.preventDefault()
82 | }
83 | }
84 |
85 | render() {
86 | return (
91 |
92 | {
93 | this.props.title &&
94 | {this.props.title}
95 |
96 | }
97 |
98 | {!this.props.form &&
}
99 | {this.props.content}
100 |
101 |
102 |
103 | Cancel
108 | {this.props.button || 'OK'}
113 |
114 |
115 |
)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/Modal/index.scss:
--------------------------------------------------------------------------------
1 | .Modal {
2 | position: fixed;
3 | left: 0;
4 | width: 100%;
5 | z-index: 999;
6 | height: calc(100% + 100px);
7 | margin-top: -100px;
8 | }
9 |
10 | .Modal__title {
11 | font-size: 14px;
12 | font-weight: bold;
13 | margin-bottom: 10px;
14 | }
15 |
16 | .Modal__content {
17 | position: relative;
18 | width: 420px;
19 | background: #efefef;
20 | border: 1px solid #a3a3a3;
21 | border-top: 0;
22 |
23 | padding: 18px 20px 18px 100px;
24 | box-shadow: inset 1px 4px 9px -6px, 0 5px 20px rgba(0, 0, 0, 0.3);
25 |
26 | margin: 100px auto 0;
27 |
28 | font-size: 12px;
29 |
30 | .nt-button-group {
31 | margin-top: 20px;
32 | }
33 |
34 | * {
35 | -webkit-user-select: text;
36 | }
37 | }
38 |
39 | .Modal__icon {
40 | position: absolute;
41 | left: 20px;
42 | top: 22px;
43 | width: 62px;
44 | height: 57px;
45 | background: transparent url(./warning.png) left top no-repeat;
46 | background-size: 62px 57px;
47 |
48 | span {
49 | position: absolute;
50 | bottom: -6px;
51 | right: -6px;
52 | width: 34px;
53 | height: 34px;
54 | background: transparent url(./icon.png) left top no-repeat;
55 | background-size: 34px 34px;
56 | }
57 | }
58 |
59 | .Modal__form {
60 | h3 {
61 | display: none;
62 | }
63 |
64 | .ui-corner-all {
65 | padding: 0 !important;
66 | margin: 0 !important;
67 | }
68 |
69 | .form-control {
70 | position: relative;
71 | background: none;
72 | border: 0;
73 | padding: 4px 0px 14px !important;
74 |
75 | label {
76 | position: absolute;
77 | left: -90px;
78 | text-align: right;
79 | width: 80px;
80 | font-weight: normal !important;
81 | }
82 |
83 | input, select {
84 | width: 100% !important;
85 | margin: 0 !important;
86 | }
87 |
88 | .ui-state-error {
89 | position: absolute;
90 | top: 25px;
91 | opacity: 0;
92 | }
93 | }
94 |
95 | .ui-state-error {
96 | color: #ff2a1c;
97 | font-size: 12px;
98 | }
99 |
100 | // input, select {
101 | // width: 100%;
102 | // min-height: 25px;
103 | // padding: 5px 10px;
104 | // line-height: 1.6;
105 | // background-color: #fff;
106 | // border: 1px solid #ddd;
107 | // outline: 0;
108 |
109 | // &:focus {
110 | // border-radius: 4px;
111 | // border-color: #6db3fd;
112 | // box-shadow: 3px 3px 0 #6db3fd, -3px -3px 0 #6db3fd, -3px 3px 0 #6db3fd, 3px -3px 0 #6db3fd;
113 | // }
114 | // }
115 | }
116 |
117 | .modal-enter {
118 | .Modal__content {
119 | transform: translateY(-100%);
120 | }
121 | }
122 |
123 | .modal-enter.modal-enter-active {
124 | .Modal__content {
125 | transform: translateY(0);
126 | transition: transform 150ms linear;
127 | }
128 | }
129 |
130 | .modal-leave {
131 | .Modal__content {
132 | transform: translateY(0);
133 | }
134 | }
135 |
136 | .modal-leave.modal-leave-active {
137 | .Modal__content {
138 | transform: translateY(-100%);
139 | transition: transform 150ms linear;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/Modal/warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/medis/12c87a5a2cc3fd7fa616beb2eaed79413538769a/src/renderer/windows/MainWindow/InstanceContent/Modal/warning.png
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceContent/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {PureComponent} from 'react'
4 | import ConnectionSelectorContainer from './ConnectionSelectorContainer'
5 | import DatabaseContainer from './DatabaseContainer'
6 | import Modal from './Modal'
7 | import ReactCSSTransitionGroup from 'react-addons-css-transition-group'
8 |
9 | class InstanceContent extends PureComponent {
10 | constructor() {
11 | super()
12 | this.state = {}
13 | }
14 |
15 | componentDidMount() {
16 | window.showModal = modal => {
17 | this.activeElement = document.activeElement
18 | this.setState({modal})
19 |
20 | return new Promise((resolve, reject) => {
21 | this.promise = {resolve, reject}
22 | })
23 | }
24 | }
25 |
26 | modalSubmit(result) {
27 | this.promise.resolve(result)
28 | this.setState({modal: null})
29 | if (this.activeElement) {
30 | this.activeElement.focus()
31 | }
32 | }
33 |
34 | modalCancel() {
35 | this.promise.reject()
36 | this.setState({modal: null})
37 | if (this.activeElement) {
38 | this.activeElement.focus()
39 | }
40 | }
41 |
42 | componentWillUnmount() {
43 | delete window.showModal
44 | }
45 |
46 | render() {
47 | const {instances, activeInstanceKey} = this.props
48 | const contents = instances.map(instance => (
49 |
53 | {
54 | instance.get('redis')
55 | ?
56 | :
57 | }
58 |
59 | ))
60 |
61 | return (
62 |
63 |
68 | {
69 | this.state.modal &&
70 |
76 | }
77 |
78 | {contents}
79 |
80 | )
81 | }
82 | }
83 |
84 | export default InstanceContent
85 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceTabs/Tab.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react'
2 | import {SortableElement} from 'react-sortable-hoc'
3 |
4 | interface ITabProps {
5 | instanceKey: string,
6 | title?: string,
7 | active: boolean,
8 | onTabClick: (key: string) => void,
9 | onTabCloseButtonClick: (key: string) => void
10 | }
11 |
12 | function Tab({instanceKey, onTabClick, onTabCloseButtonClick, active, title = 'Quick Connect'}: ITabProps) {
13 | return {
15 | onTabClick(instanceKey)
16 | }}
17 | className={active ? 'tab-item active' : 'tab-item'}
18 | >
19 | {title}
20 | onTabCloseButtonClick(instanceKey)}>
23 |
24 | }
25 |
26 | export default memo(SortableElement(Tab))
27 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceTabs/Tabs.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react'
2 | import {SortableContainer} from 'react-sortable-hoc'
3 | import Tab from './Tab'
4 |
5 | interface ITabsProps {
6 | instances: any
7 | activeInstanceKey: string
8 | onTabSelect: (key: string) => void
9 | onTabClose: (key: string) => void
10 |
11 | }
12 | function Tabs({instances, activeInstanceKey, onTabSelect, onTabClose}: ITabsProps) {
13 | return (
14 |
15 | {instances.map((instance, index) => {
16 | const key = instance.get('key')
17 | return
26 | })}
27 |
28 | )
29 | }
30 |
31 | export default memo(SortableContainer(Tabs))
32 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceTabs/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo} from 'react'
2 | import Tabs from './Tabs'
3 |
4 | require('./main.scss')
5 |
6 | function isModalShown() {
7 | return $('.Modal').length > 0
8 | }
9 |
10 | let display = 'flex'
11 |
12 | interface IInstanceTabsProps {
13 | instances: any
14 | activeInstanceKey: string
15 | onCreateInstance: any
16 | onSelectInstance: any
17 | onDelInstance: any
18 | onMoveInstance: any
19 | }
20 |
21 | function InstanceTabs({
22 | onCreateInstance, onSelectInstance, onDelInstance, instances, activeInstanceKey, onMoveInstance
23 | }: IInstanceTabsProps) {
24 | const handleAddButtonClick = () => {
25 | if (!isModalShown()) {
26 | onCreateInstance()
27 | }
28 | }
29 |
30 | const handleTabSelect = (key: string) => {
31 | if (!isModalShown()) {
32 | onSelectInstance(key)
33 | }
34 | }
35 |
36 | const handleTabClose = (key: string) => {
37 | if (!isModalShown()) {
38 | onDelInstance(key)
39 | }
40 | }
41 |
42 | const currentDisplay = instances.count() === 1 ? 'none' : 'flex'
43 | if (display !== currentDisplay) {
44 | display = currentDisplay
45 | setTimeout(() => $(window).trigger('resize'), 0)
46 | }
47 |
48 | return
49 |
50 |
{
61 | if (oldIndex !== newIndex) {
62 | onMoveInstance(instances.getIn([oldIndex, 'key']), instances.getIn([newIndex, 'key']))
63 | }
64 | }}
65 | shouldCancelStart={(e) => (e.target as any).nodeName.toUpperCase() === 'SPAN'}
66 | />
67 |
68 | {'+'}
69 |
70 |
71 |
72 | }
73 |
74 | export default memo(InstanceTabs)
75 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/InstanceTabs/main.scss:
--------------------------------------------------------------------------------
1 | .instance-tabs {
2 | * {
3 | -webkit-user-select: none;
4 | }
5 | display: flex;
6 |
7 | li {
8 | position: relative;
9 | flex-grow: 1;
10 | background: #bebebe;
11 | color: #424242;
12 | border: 1px solid #a0a0a0;
13 | border-right: none;
14 | text-align: center;
15 | line-height: 22px;
16 | cursor: default;
17 |
18 | &:first-child {
19 | border-left: none;
20 | }
21 |
22 | &.is-active,
23 | &.is-active:hover {
24 | background: #d3d3d3;
25 | color: #000000;
26 | border-top-color: #d3d3d3;
27 |
28 | .rdTabCloseIcon:hover {
29 | background: #c0c0c0;
30 | color: #6c6c6c;
31 | }
32 | }
33 |
34 | &:hover {
35 | background: #b2b2b2;
36 | color: #3e3e3e;
37 | .rdTabCloseIcon {
38 | display: block;
39 | }
40 | }
41 | }
42 | }
43 |
44 | .instance-tabs__add {
45 | flex-grow: 0 !important;
46 | flex-basis: 24px;
47 | &:hover {
48 | background: #bebebe !important;
49 | color: #424242 !important;
50 | }
51 | }
52 |
53 | .rdTabCloseIcon {
54 | position: absolute;
55 | left: 4px;
56 | top: 4px;
57 | display: none;
58 | border-radius: 2px;
59 | line-height: 1em;
60 | width: 14px;
61 | height: 14px;
62 |
63 | &:hover {
64 | color: #5b5b5b;
65 | background: #a2a2a2;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/entry.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | require('../../photon/css/photon.min.css')
4 | require('../../../../node_modules/fixed-data-table-contextmenu/dist/fixed-data-table.css')
5 |
6 | import ReactDOM from 'react-dom'
7 | import MainWindow from './'
8 | import {ipcRenderer} from 'electron'
9 | import store from 'Redux/store'
10 | import * as actions from 'Redux/actions'
11 |
12 | require('../../styles/global.scss')
13 |
14 | window.$ = window.jQuery = require('jquery');
15 | window.Buffer = global.Buffer;
16 |
17 | ipcRenderer.on('action', (evt, action) => {
18 | if ($('.Modal').length && action.indexOf('Instance') !== -1) {
19 | return
20 | }
21 |
22 | store.skipPersist = true
23 | store.dispatch(actions[action]())
24 | store.skipPersist = false
25 | })
26 |
27 | ReactDOM.render(MainWindow, document.body.appendChild(document.createElement('div')))
28 |
--------------------------------------------------------------------------------
/src/renderer/windows/MainWindow/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {PureComponent} from 'react'
4 | import {createSelector} from 'reselect'
5 | import {Provider, connect} from 'react-redux'
6 | import InstanceTabs from './InstanceTabs'
7 | import InstanceContent from './InstanceContent'
8 | import DocumentTitle from 'react-document-title'
9 | import {createInstance, selectInstance, delInstance, moveInstance} from 'Redux/actions'
10 | import store from 'Redux/store'
11 |
12 | class MainWindow extends PureComponent {
13 | componentDidMount() {
14 | $(window).on('keydown.redis', this.onHotKey.bind(this))
15 | }
16 |
17 | componentWillUnmount() {
18 | $(window).off('keydown.redis')
19 | }
20 |
21 | onHotKey(e) {
22 | const {instances, selectInstance} = this.props
23 | if (!e.ctrlKey && e.metaKey) {
24 | const code = e.keyCode
25 | if (code >= 49 && code <= 57) {
26 | const number = code - 49
27 | if (number === 8) {
28 | const instance = instances.get(instances.count() - 1)
29 | if (instance) {
30 | selectInstance(instance.get('key'))
31 | return false
32 | }
33 | } else {
34 | const instance = instances.get(number)
35 | if (instance) {
36 | selectInstance(instance.get('key'))
37 | return false
38 | }
39 | }
40 | }
41 | }
42 | return true
43 | }
44 |
45 | getTitle() {
46 | const {activeInstance} = this.props
47 | if (!activeInstance) {
48 | return ''
49 | }
50 | const version = activeInstance.get('version')
51 | ? `(Redis ${activeInstance.get('version')}) `
52 | : ''
53 |
54 | return version + activeInstance.get('title')
55 | }
56 |
57 | render() {
58 | const {instances, activeInstance, createInstance,
59 | selectInstance, delInstance, moveInstance} = this.props
60 |
61 | return (
62 |
63 |
71 |
75 |
76 | )
77 | }
78 | }
79 |
80 | const selector = createSelector(
81 | state => state.instances,
82 | state => state.activeInstanceKey,
83 | (instances, activeInstanceKey) => {
84 | return {
85 | instances,
86 | activeInstance: instances.find(instance => instance.get('key') === activeInstanceKey)
87 | }
88 | }
89 | )
90 |
91 | const mapDispatchToProps = {
92 | createInstance,
93 | selectInstance,
94 | delInstance,
95 | moveInstance
96 | }
97 |
98 | const MainWindowContainer = connect(selector, mapDispatchToProps)(MainWindow)
99 |
100 | export default
101 |
102 |
103 |
--------------------------------------------------------------------------------
/src/renderer/windows/PatternManagerWindow/app.scss:
--------------------------------------------------------------------------------
1 | .patternList {
2 | background: #fff;
3 | border: 1px solid #c5c5c5;
4 | width: 210px;
5 | position: absolute;
6 | top: 20px;
7 | left: 20px;
8 | height: 236px;
9 |
10 | footer {
11 | position: absolute;
12 | bottom: 0;
13 | left: 0;
14 | width: 208px;
15 | height: 19px;
16 | background: #fafafa;
17 | border-top: 1px solid #b4b4b4;
18 | button {
19 | width: 23px;
20 | height: 18px;
21 | border-radius: 0;
22 | padding: 0;
23 | border-left: 1px solid #b4b4b4;
24 | border-right: 1px solid #b4b4b4;
25 | margin-left: -1px;
26 | border-top: 0;
27 | border-bottom: 0;
28 | background: #fafafa;
29 |
30 | &.is-disabled {
31 | color: #bfbfbf;
32 | }
33 | }
34 | }
35 |
36 | .nav-group-item:active {
37 | background-color: #116cd6 !important;
38 | color: #fff;
39 | }
40 | }
41 |
42 | .nav-group-item {
43 | padding: 0 5px;
44 |
45 | &.sortable-chosen {
46 | color: #000;
47 |
48 | &.is-active {
49 | background: #116cd6 !important;
50 | color: #fff;
51 | }
52 | }
53 |
54 | &.is-active {
55 | background: #116cd6;
56 | color: #fff;
57 | }
58 | }
59 |
60 | .form {
61 | position: absolute !important;
62 | right: 20px;
63 | top: 20px;
64 | width: 328px;
65 | height: 236px;
66 | }
67 |
--------------------------------------------------------------------------------
/src/renderer/windows/PatternManagerWindow/entry.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | require('../../photon/css/photon.min.css')
4 |
5 | import React from 'react'
6 | import ReactDOM from 'react-dom'
7 | import {Provider} from 'react-redux'
8 | import PatternManagerWindow from './'
9 | import store from 'Redux/store'
10 | import * as actions from 'Redux/actions'
11 | import {remote, ipcRenderer} from 'electron'
12 |
13 | require('../../styles/global.scss')
14 |
15 | window.$ = window.jQuery = require('jquery');
16 |
17 | ipcRenderer.on('action', (evt, action) => {
18 | if (type === 'delInstance') {
19 | remote.getCurrentWindow().close()
20 | return
21 | }
22 |
23 | store.skipPersist = true
24 | store.dispatch(actions[action]())
25 | store.skipPersist = false
26 | })
27 |
28 | ReactDOM.render(
29 |
30 |
31 | ,
32 | document.body.appendChild(document.createElement('div'))
33 | )
34 |
--------------------------------------------------------------------------------
/src/renderer/windows/PatternManagerWindow/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {createPattern, updatePattern, removePattern} from 'Redux/actions'
4 | import {List} from 'immutable'
5 |
6 | require('./app.scss')
7 |
8 | const connectionKey = getParameterByName('arg')
9 |
10 | class App extends React.Component {
11 | constructor(props, context) {
12 | super(props, context)
13 | this.state = {index: 0}
14 | }
15 |
16 | handleChange(property, e) {
17 | this.setState({[property]: e.target.value})
18 | }
19 |
20 | select(index) {
21 | this.setState({
22 | index,
23 | name: null,
24 | value: null
25 | })
26 | }
27 |
28 | renderPatternForm(activePattern) {
29 | if (!activePattern) {
30 | return null
31 | }
32 | return
37 |
38 | Name:
39 |
45 |
46 |
47 | Pattern:
48 |
54 |
55 |
56 | {
60 | this.props.updatePattern(connectionKey, this.state.index, {
61 | name: this.state.name || activePattern.get('name'),
62 | value: this.state.value || activePattern.get('value')
63 | })
64 | alert('Save Successfully')
65 | }}
66 | >Save
67 |
68 |
69 | }
70 |
71 | render() {
72 | const {patterns, createPattern, removePattern} = this.props
73 | const activePattern = patterns.get(this.state.index)
74 | return (
75 |
76 |
87 |
88 | {
90 | const index = patterns.size
91 | createPattern(connectionKey)
92 | this.select(index)
93 | }}
94 | >+
95 | {
98 | if (activePattern) {
99 | removePattern(connectionKey, this.state.index)
100 | this.select(this.state.index > 0 ? this.state.index - 1 : 0)
101 | }
102 | }}
103 | >-
104 |
105 |
106 | {this.renderPatternForm(activePattern)}
107 |
)
108 | }
109 | }
110 |
111 | function mapStateToProps(state) {
112 | return {
113 | patterns: state.patterns.get(connectionKey, List())
114 | }
115 | }
116 |
117 | const mapDispatchToProps = {
118 | updatePattern,
119 | createPattern,
120 | removePattern
121 | }
122 |
123 | export default connect(mapStateToProps, mapDispatchToProps)(App)
124 |
125 | function getParameterByName(name) {
126 | name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
127 | const regex = new RegExp('[\\?&]' + name + '=([^]*)')
128 | const results = regex.exec(location.search)
129 | return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '))
130 | }
131 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "target": "es6",
5 | "lib": [
6 | "es6"
7 | ],
8 | "moduleResolution": "node",
9 | "types": [
10 | "node"
11 | ],
12 | "typeRoots": [
13 | "node_modules/@types"
14 | ],
15 | "module": "commonjs",
16 | "jsx": "preserve",
17 | "allowSyntheticDefaultImports": true,
18 | "esModuleInterop": true
19 | },
20 | "include": [
21 | "./src/**/*"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {resolve} = require('path')
4 | const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer')
5 | const HtmlWebpackPlugin = require('html-webpack-plugin')
6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
7 | const {CheckerPlugin} = require('awesome-typescript-loader')
8 | const webpack = require('webpack')
9 |
10 | const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'
11 | const watch = process.env.WEBPACK_WATCH === 'true'
12 |
13 | const distPath = resolve(__dirname, 'dist')
14 |
15 | const base = {
16 | mode, watch,
17 | output: {
18 | path: distPath,
19 | chunkFilename: '[name].chunk.js',
20 | filename: '[name].js'
21 | },
22 | node: {
23 | Buffer: false,
24 | buffer: false,
25 | __dirname: false,
26 | __filename: false,
27 | },
28 | module: {
29 | rules: [{
30 | test: /\.(ts|tsx)$/,
31 | use: [{
32 | loader: 'awesome-typescript-loader',
33 | options: {
34 | reportFiles: ['src/**/*.{ts,tsx}'],
35 | useCache: true,
36 | useBabel: true,
37 | babelCore: '@babel/core'
38 | }
39 | }],
40 | exclude: /node_modules/
41 | }, {
42 | test: /\.(js|jsx)$/,
43 | exclude: /node_modules/,
44 | use: ['babel-loader']
45 | }, {
46 | test: /\.scss$/,
47 | use: [
48 | MiniCssExtractPlugin.loader,
49 | 'css-loader',
50 | 'sass-loader'
51 | ]
52 | }, {
53 | test: /\.css$/,
54 | use: [
55 | MiniCssExtractPlugin.loader,
56 | 'css-loader'
57 | ]
58 | }, {
59 | test: /\.(png|jpg)$/,
60 | use: [{
61 | loader: "file-loader"
62 | }]
63 | }, {
64 | test: /\.(eot|woff|ttf)$/,
65 | use: [{
66 | loader: "file-loader"
67 | }]
68 | }]
69 | },
70 | externals: {
71 | 'system': '{}', // jsonlint
72 | 'file': '{}' // jsonlint
73 | },
74 | }
75 |
76 | const renderPlugins = [
77 | new HtmlWebpackPlugin({title: 'Medis', chunks: ['main'], filename: 'main.html'}),
78 | new HtmlWebpackPlugin({title: 'Manage Patterns', chunks: ['patternManager'], filename: 'patternManager.html'}),
79 | new MiniCssExtractPlugin({filename: '[name].css'}),
80 | new CheckerPlugin(),
81 | new webpack.ProvidePlugin({React: 'react'}),
82 | ]
83 | if (mode === 'production') {
84 | renderPlugins.push(new BundleAnalyzerPlugin())
85 | }
86 | const renderer = Object.assign({}, base, {
87 | target: 'electron-renderer',
88 | output: Object.assign({}, base.output, {
89 | path: resolve(base.output.path, 'renderer')
90 | }),
91 | entry: {
92 | main: resolve(__dirname, 'src/renderer/windows/MainWindow/entry.jsx'),
93 | patternManager: resolve(__dirname, 'src/renderer/windows/PatternManagerWindow/entry.jsx')
94 | },
95 | plugins: renderPlugins,
96 | resolve: {
97 | alias: {
98 | Redux: resolve(__dirname, 'src/renderer/redux/'),
99 | Utils: resolve(__dirname, 'src/renderer/utils/'),
100 | },
101 | extensions: ['.js', '.jsx', '.ts', '.tsx']
102 | }
103 | })
104 |
105 | const main = Object.assign({}, base, {
106 | target: 'electron-main',
107 | output: Object.assign({}, base.output, {
108 | path: resolve(base.output.path, 'main')
109 | }),
110 | entry: {
111 | index: resolve(__dirname, 'src/main/index.ts')
112 | },
113 | resolve: {
114 | extensions: ['.js', '.jsx', '.ts', '.tsx']
115 | }
116 | })
117 |
118 | module.exports = [main, renderer]
119 |
--------------------------------------------------------------------------------