├── icns
├── Icon1024.png
├── MyIcon.icns
└── generate
├── resources
└── mac
│ └── atom.icns
├── client
├── images
│ └── design.sketch
├── photon
│ ├── fonts
│ │ ├── photon-entypo.eot
│ │ ├── photon-entypo.ttf
│ │ └── photon-entypo.woff
│ └── template-app
│ │ ├── package.json
│ │ ├── js
│ │ └── menu.js
│ │ ├── app.js
│ │ └── index.html
├── redux
│ ├── middlewares
│ │ ├── index.js
│ │ └── createThunkReplyMiddleware.js
│ ├── actions
│ │ ├── sizes.js
│ │ ├── index.js
│ │ ├── patterns.js
│ │ ├── favorites.js
│ │ ├── instances.js
│ │ └── connection.js
│ ├── reducers
│ │ ├── sizes.js
│ │ ├── index.js
│ │ ├── activeInstanceKey.js
│ │ ├── favorites.js
│ │ ├── patterns.js
│ │ └── instances.js
│ ├── store.js
│ └── persistEnhancer.js
├── storage
│ ├── index.js
│ ├── Sizes.js
│ ├── Patterns.js
│ └── Favorites.js
├── windows
│ ├── MainWindow
│ │ ├── components
│ │ │ ├── InstanceContent
│ │ │ │ ├── components
│ │ │ │ │ ├── Modal
│ │ │ │ │ │ ├── icon.png
│ │ │ │ │ │ ├── warning.png
│ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ └── index.jsx
│ │ │ │ │ ├── DatabaseContainer
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ ├── ContentEditable
│ │ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ │ ├── Content
│ │ │ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ │ │ ├── KeyContent
│ │ │ │ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ │ │ │ ├── components
│ │ │ │ │ │ │ │ │ │ │ ├── BaseContent
│ │ │ │ │ │ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ │ │ │ │ │ ├── StringContent.jsx
│ │ │ │ │ │ │ │ │ │ │ ├── SortHeaderCell.jsx
│ │ │ │ │ │ │ │ │ │ │ ├── Editor
│ │ │ │ │ │ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ │ │ │ │ │ ├── SetContent.jsx
│ │ │ │ │ │ │ │ │ │ │ ├── ListContent.jsx
│ │ │ │ │ │ │ │ │ │ │ └── HashContent.jsx
│ │ │ │ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ │ │ │ ├── Config
│ │ │ │ │ │ │ │ │ │ └── index.scss
│ │ │ │ │ │ │ │ │ ├── TabBar
│ │ │ │ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ │ │ │ ├── Terminal
│ │ │ │ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ │ │ │ └── Footer.jsx
│ │ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ │ ├── AddButton
│ │ │ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ │ │ └── index.scss
│ │ │ │ │ │ │ └── KeyBrowser
│ │ │ │ │ │ │ │ ├── index.jsx
│ │ │ │ │ │ │ │ └── components
│ │ │ │ │ │ │ │ ├── PatternList
│ │ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ │ │ ├── KeyList
│ │ │ │ │ │ │ │ └── index.scss
│ │ │ │ │ │ │ │ └── Footer.jsx
│ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ └── index.jsx
│ │ │ │ │ └── ConnectionSelectorContainer
│ │ │ │ │ │ ├── components
│ │ │ │ │ │ ├── Config
│ │ │ │ │ │ │ ├── index.scss
│ │ │ │ │ │ │ └── index.jsx
│ │ │ │ │ │ └── Favorite.jsx
│ │ │ │ │ │ └── index.jsx
│ │ │ │ └── index.jsx
│ │ │ └── InstanceTabs
│ │ │ │ ├── components
│ │ │ │ └── draggable-tab
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ ├── components
│ │ │ │ │ ├── Tab.js
│ │ │ │ │ └── Tabs.js
│ │ │ │ │ └── styles
│ │ │ │ │ └── main.scss
│ │ │ │ └── index.jsx
│ │ ├── entry.jsx
│ │ └── index.jsx
│ └── PatternManagerWindow
│ │ ├── entry.jsx
│ │ ├── app.scss
│ │ └── index.jsx
├── styles
│ ├── global.scss
│ ├── photon.scss
│ └── native.scss
├── utils.js
└── vendors
│ ├── jquery.terminal
│ └── index.css
│ └── jquery.context-menu
│ └── index.css
├── .gitignore
├── child.plist
├── server
├── windows
│ ├── patternManager.html
│ └── main.html
├── tools
│ └── index.js
├── main.js
├── windowManager.js
└── menu.js
├── parent.plist
├── webpack.production.config.js
├── bin
├── release
├── conventional-github-releaser.js
└── pack.js
├── LICENSE
├── webpack.config.js
├── package.json
├── README.md
├── .eslintrc
└── CHANGELOG.md
/icns/Icon1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/icns/Icon1024.png
--------------------------------------------------------------------------------
/icns/MyIcon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/icns/MyIcon.icns
--------------------------------------------------------------------------------
/resources/mac/atom.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/resources/mac/atom.icns
--------------------------------------------------------------------------------
/client/images/design.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/client/images/design.sketch
--------------------------------------------------------------------------------
/client/photon/fonts/photon-entypo.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/client/photon/fonts/photon-entypo.eot
--------------------------------------------------------------------------------
/client/photon/fonts/photon-entypo.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/client/photon/fonts/photon-entypo.ttf
--------------------------------------------------------------------------------
/client/photon/fonts/photon-entypo.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/client/photon/fonts/photon-entypo.woff
--------------------------------------------------------------------------------
/client/redux/middlewares/index.js:
--------------------------------------------------------------------------------
1 | import createThunkReplyMiddleware from './createThunkReplyMiddleware'
2 |
3 | export {createThunkReplyMiddleware}
4 |
--------------------------------------------------------------------------------
/client/redux/actions/sizes.js:
--------------------------------------------------------------------------------
1 | import {createAction} from 'Utils';
2 |
3 | export const setSize = createAction('SET_SIZE', (type, value) => ({type, value: Number(value)}))
4 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | Medis.app
3 | sign
4 | Medis.pkg
5 | .DS_Store
6 | main.js
7 | pattern-manager.js
8 | npm-debug.log
9 | *.zip
10 | out
11 | *.provisionprofile
12 | package-lock.json
13 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/Modal/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/client/windows/MainWindow/components/InstanceContent/components/Modal/icon.png
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/Modal/warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sinajia/medis/HEAD/client/windows/MainWindow/components/InstanceContent/components/Modal/warning.png
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/ContentEditable/index.scss:
--------------------------------------------------------------------------------
1 | .ContentEditable {
2 | [contenteditable="true"] {
3 | background: #fff !important;
4 | color: #333;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceTabs/components/draggable-tab/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import Tab from './components/Tab'
4 | import Tabs from './components/Tabs'
5 |
6 | require('./styles/main.scss')
7 |
8 | export {Tab, Tabs}
9 |
--------------------------------------------------------------------------------
/client/photon/template-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "proton-template-app",
3 | "version": "1.0.0",
4 | "description": "A simple template app for Proton",
5 | "main": "app.js",
6 | "author": "Connor Sears",
7 | "scripts": {
8 | "start": "electron ."
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/child.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.inherit
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/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 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/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 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/server/windows/patternManager.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Manage Patterns
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/AddButton/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | require('./index.scss')
4 |
5 | export default class AddButton extends React.PureComponent {
6 | render() {
7 | return (
8 | {this.props.title}
9 | {
10 | this.props.reload &&
11 | }
12 | +
13 |
)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/webpack.production.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const webpack = require('webpack');
4 |
5 | const config = require('./webpack.config');
6 |
7 | const plugins = [];
8 |
9 | plugins.push(
10 | new webpack.optimize.UglifyJsPlugin({
11 | compressor: {
12 | warnings: false
13 | }
14 | }),
15 | new webpack.optimize.DedupePlugin(),
16 | new webpack.DefinePlugin({
17 | 'process.env': { NODE_ENV: '"production"' }
18 | }),
19 | new webpack.NoErrorsPlugin()
20 | );
21 |
22 | config.plugins = plugins;
23 |
24 | module.exports = config;
25 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/redux/actions/patterns.js:
--------------------------------------------------------------------------------
1 | import {createAction} from 'Utils';
2 | import {fromJS} from 'immutable'
3 | import {Patterns} from '../../storage'
4 |
5 |
6 | export const createPattern = createAction('CREATE_PATTERN', (conn) => {
7 | const key = `pattern-${Math.round(Math.random() * 100000)}`
8 | return Object.assign({key, conn})
9 | })
10 |
11 | export const reloadPatterns = createAction('RELOAD_PATTERNS', Patterns.get)
12 | export const removePattern = createAction('REMOVE_PATTERN', (conn, index) => ({conn, index}))
13 | export const updatePattern = createAction('UPDATE_PATTERN', (conn, index, data) => ({conn, index, data}))
14 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/entry.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import MainWindow from './'
6 | import {ipcRenderer} from 'electron'
7 | import store from 'Redux/store'
8 | import * as actions from 'Redux/actions'
9 |
10 | require('../../styles/global.scss')
11 |
12 | ipcRenderer.on('action', (evt, action) => {
13 | if ($('.Modal').length && action.indexOf('Instance') !== -1) {
14 | return
15 | }
16 |
17 | store.skipPersist = true
18 | store.dispatch(actions[action]())
19 | store.skipPersist = false
20 | })
21 |
22 | ReactDOM.render(MainWindow, document.getElementById('content'))
23 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceTabs/components/draggable-tab/components/Tab.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import PropTypes from 'prop-types';
5 |
6 | class Tab extends React.Component {
7 | constructor(props) {
8 | super(props);
9 | this.state = {
10 | };
11 | }
12 |
13 | render() {
14 | return this.props.children;
15 | }
16 | }
17 |
18 | Tab.defaultProps = {
19 | title: 'Quick Connect',
20 | disableClose: false
21 | };
22 |
23 | Tab.propTypes = {
24 | title: PropTypes.oneOfType([
25 | PropTypes.string,
26 | PropTypes.element
27 | ]).isRequired,
28 | disableClose: PropTypes.bool
29 | };
30 |
31 | export default Tab;
32 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/ConnectionSelectorContainer/components/Config/index.scss:
--------------------------------------------------------------------------------
1 | #sshPassword {
2 | padding-right: 32px;
3 | }
4 |
5 | .ssh-key {
6 | height: 19px;
7 | line-height: 20px;
8 | padding: 0;
9 | text-align: center;
10 | width: 30px;
11 | position: relative;
12 | top: -1px;
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/windows/PatternManagerWindow/entry.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import {Provider} from 'react-redux'
6 | import PatternManagerWindow from './'
7 | import store from 'Redux/store'
8 | import * as actions from 'Redux/actions'
9 | import {remote, ipcRenderer} from 'electron'
10 |
11 | require('../../styles/global.scss')
12 |
13 | ipcRenderer.on('action', (evt, action) => {
14 | if (type === 'delInstance') {
15 | remote.getCurrentWindow().close()
16 | return
17 | }
18 |
19 | store.skipPersist = true
20 | store.dispatch(actions[action]())
21 | store.skipPersist = false
22 | })
23 |
24 | ReactDOM.render(
25 |
26 |
27 | ,
28 | document.getElementById('content')
29 | )
30 |
--------------------------------------------------------------------------------
/client/photon/template-app/js/menu.js:
--------------------------------------------------------------------------------
1 | var {remote} = require('electron')
2 | var Menu = remote.require('menu')
3 | var MenuItem = remote.require('menu-item')
4 |
5 | // Build our new menu
6 | var menu = new Menu()
7 | menu.append(new MenuItem({
8 | label: 'Delete',
9 | click: function() {
10 | // Trigger an alert when menu item is clicked
11 | alert('Deleted')
12 | }
13 | }))
14 | menu.append(new MenuItem({
15 | label: 'More Info...',
16 | click: function() {
17 | // Trigger an alert when menu item is clicked
18 | alert('Here is more information')
19 | }
20 | }))
21 |
22 | // Add the listener
23 | document.addEventListener('DOMContentLoaded', function () {
24 | document.querySelector('.js-context-menu').addEventListener('click', function (event) {
25 | menu.popup(remote.getCurrentWindow());
26 | })
27 | })
28 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/server/tools/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {app} = require('electron');
4 | const path = require('path');
5 | const del = require('del');
6 |
7 | module.exports.label = {
8 | label: 'Tools',
9 | submenu: [{
10 | label: 'Clear Favorites (Windows only)',
11 | click() {
12 | if('win32' === process.platform) {
13 | app.relaunch({args: process.argv.slice(1).concat(['--clear-favorites'])});
14 | app.exit(0);
15 | }
16 | }
17 | }]
18 | };
19 |
20 | exports.delFavo = function* () {
21 | var localStorage = path.join(`${app.getPath('appData')}`, 'Medis', 'Local Storage');
22 | yield del([`${localStorage}`], {force: true});
23 | }
24 |
25 | exports.shouldRelaunch = function () {
26 | if(process.argv.indexOf('--clear-favorites') !== -1) {
27 | return true;
28 | }
29 | return false;
30 | }
31 |
32 | exports.args = function () {
33 | var args = process.argv.slice(1).filter(item => {
34 | return item !== '--clear-favorites';
35 | });
36 | return args;
37 | }
--------------------------------------------------------------------------------
/server/windows/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Medis
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/bin/release:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | . "bin/deploy"
4 |
5 | APP_PATH="Medis.app"
6 | FRAMEWORKS_PATH="$APP_PATH/Contents/Frameworks"
7 | INFO="$APP_PATH/Contents/Info.plist"
8 |
9 | [ -z "$CONVENTIONAL_GITHUB_RELEASER_TOKEN" ] && echo "Need to set Token" && exit 1;
10 | version=`cat package.json | json version` &&
11 | git commit -am "chore(release): %s" &&
12 | perl -pi -e "s/#version#/$version/g" "$INFO" &&
13 | zip --symlinks -r9 "medis.zip" "$APP_PATH" &&
14 | conventional-changelog -i CHANGELOG.md -s -p angular &&
15 | git add CHANGELOG.md &&
16 | git commit -m"docs(CHANGELOG): $version" &&
17 | git tag -a -m "chore(release): %s" $version &&
18 | git push --follow-tags &&
19 | node "bin/conventional-github-releaser" &&
20 | TARGET="medis-v$version-mac-x64.zip" &&
21 | mv "medis.zip" $TARGET &&
22 | echo "**************************************************************************"
23 | echo "Upload $TARGET to the https://github.com/luin/medis/releases/edit/v$version"
24 | echo "**************************************************************************"
25 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/bin/conventional-github-releaser.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const conventionalGithubReleaser = require('conventional-github-releaser');
4 | const path = require('path');
5 | // const Github = require('github');
6 | const pkg = require('../package.json');
7 |
8 | // const github = new Github({
9 | // version: '3.0.0'
10 | // });
11 |
12 | const AUTH = {
13 | type: 'oauth',
14 | token: process.env.CONVENTIONAL_GITHUB_RELEASER_TOKEN
15 | };
16 |
17 | conventionalGithubReleaser(AUTH, {
18 | preset: 'angular',
19 | pkg: {
20 | path: path.join(__dirname, '..', 'package.json')
21 | }
22 | }, function (err, res) {
23 | process.exit(0);
24 | // console.log(err, res);
25 | // const id = res[0].value.id;
26 | // github.authenticate(AUTH);
27 | // github.releases.uploadAsset({
28 | // id,
29 | // owner: 'luin',
30 | // repo: 'medis',
31 | // name: `medis-v${pkg.version}-mac-x64.zip`,
32 | // filePath: path.join(__dirname, '..', 'medis.zip')
33 | // }, function (err, res) {
34 | // console.log(err, res);
35 | // process.exit(0);
36 | // });
37 | });
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 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 |
--------------------------------------------------------------------------------
/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 | packager({
7 | dir: path.join(__dirname, '..'),
8 | appCopyright: '© 2017, Zihua Li',
9 | asar: true,
10 | overwrite: true,
11 | electronVersion: pkg.electronVersion,
12 | icon: path.join(__dirname, '..', 'icns', 'MyIcon'),
13 | out: path.join(__dirname, '..', 'out'),
14 | platform: 'mas',
15 | appBundleId: `li.zihua.${pkg.name}`,
16 | appCategoryType: 'public.app-category.developer-tools',
17 | osxSign: {
18 | type: process.env.NODE_ENV === 'production' ? 'distribution' : 'development',
19 | entitlements: path.join(__dirname, '..', 'parent.plist'),
20 | 'entitlements-inherit': path.join(__dirname, '..', 'child.plist')
21 | }
22 | }, function (err, res) {
23 | if (err) {
24 | throw err;
25 | }
26 |
27 | const app = path.join(res[0], `${pkg.productName}.app`)
28 | console.log('flating...', app)
29 | flat({ app }, function done (err) {
30 | if (err) {
31 | throw err
32 | }
33 | process.exit(0);
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/StringContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from './BaseContent'
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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/Terminal/index.scss:
--------------------------------------------------------------------------------
1 | .Terminal {
2 | overflow: auto;
3 | * {
4 | -webkit-user-select: text;
5 | }
6 |
7 | .number {
8 | color: #78CF8A;
9 | }
10 |
11 | .string {
12 | color: #8f9d6a;
13 | }
14 |
15 | .array-resp, .object-resp {
16 | margin: 0;
17 | padding: 0;
18 |
19 | li {
20 | display: flex;
21 | span {
22 | color: #3d3d3d;
23 | min-width: 28px;
24 | text-align: right;
25 | margin-right: 10px;
26 | }
27 | div {
28 | flex: 1;
29 | }
30 | }
31 | }
32 |
33 | .object-resp li span {
34 | font-weight: bold;
35 | color: #cda869;
36 | }
37 |
38 | .null {
39 | color: #cf7ea9;
40 | }
41 |
42 | .error {
43 | color: #ee6868 !important;
44 | }
45 |
46 | .list {
47 | color: #8f9d6a;
48 | }
49 |
50 | .monitor {
51 | color: #8f9d6a;
52 | .time {
53 | color: #3d3d3d;
54 | margin-right: 4px;
55 | }
56 |
57 | .command-name {
58 | color: #cf7ea9;
59 | }
60 |
61 | .command-key {
62 | color: #cda869;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/client/utils.js:
--------------------------------------------------------------------------------
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) {
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, 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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/SortHeaderCell.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {PureComponent} from 'react'
4 | import {Cell} from 'fixed-data-table-contextmenu'
5 |
6 | export default class SortHeaderCell extends PureComponent {
7 | handleClick(evt) {
8 | this.props.onOrderChange(!this.props.desc)
9 | evt.preventDefault()
10 | evt.stopPropagation()
11 | }
12 |
13 | render() {
14 | return (
17 |
20 | {this.props.title}
21 | {
22 |
28 | }
29 |
30 | | )
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/client/redux/reducers/patterns.js:
--------------------------------------------------------------------------------
1 | import {handleActions} from 'Utils'
2 | import {
3 | createPattern,
4 | removePattern,
5 | updatePattern,
6 | reorderPatterns,
7 | reloadPatterns
8 | } from 'Redux/actions'
9 | import {Patterns} from '../../storage'
10 | import {Map, List, fromJS} from 'immutable'
11 |
12 | function PatternFactory(data) {
13 | return Map(Object.assign({value: '*', name: '*'}, data))
14 | }
15 |
16 | export const patterns = handleActions(fromJS(Patterns.get()), {
17 | [createPattern](state, {conn, key}) {
18 | return state.update(conn, List(), patterns => patterns.push(PatternFactory({key})))
19 | },
20 | [removePattern](state, {conn, index}) {
21 | return state.update(conn, List(), patterns => patterns.remove(index))
22 | },
23 | [updatePattern](state, {conn, index, data}) {
24 | return state.update(conn, List(), patterns => patterns.update(index, item => item.merge(data)))
25 | },
26 | [reorderPatterns](state, {conn, from, to}) {
27 | return state.update(conn, List(), patterns => {
28 | const target = patterns.get(from);
29 | return patterns.splice(from, 1).splice(to, 0, target);
30 | })
31 | },
32 | [reloadPatterns](state, data) {
33 | return fromJS(data)
34 | }
35 | })
36 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/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 |
--------------------------------------------------------------------------------
/client/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 |
37 | .nav-group-item {
38 | padding: 0 5px;
39 |
40 | &.sortable-chosen {
41 | color: #000;
42 |
43 | &.is-active {
44 | background: #116cd6 !important;
45 | color: #fff;
46 | }
47 | }
48 |
49 | &.is-active {
50 | background: #116cd6;
51 | color: #fff;
52 | }
53 | }
54 |
55 | .form {
56 | position: absolute !important;
57 | right: 20px;
58 | top: 20px;
59 | width: 328px;
60 | height: 236px;
61 | }
62 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceTabs/components/draggable-tab/styles/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/TabBar/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 |
5 | require('./index.scss')
6 |
7 | class Content extends React.PureComponent {
8 | constructor() {
9 | super()
10 |
11 | this.tabs = [
12 | 'Content',
13 | 'Terminal',
14 | 'Config'
15 | ]
16 |
17 | this.state = {activeTab: 'Content'}
18 | }
19 |
20 | render() {
21 | return (
22 | {
23 | this.tabs.map(tab => {
24 | return (
{
28 | this.setState({activeTab: tab})
29 | this.props.onSelectTab(tab)
30 | }}
31 | >
32 | {
33 | (() => {
34 | if (tab === 'Content') {
35 | return
36 | } else if (tab === 'Terminal') {
37 | return
38 | } else if (tab === 'Config') {
39 | return
40 | }
41 | })()
42 | }
43 | {tab}
44 |
)
45 | })
46 | }
47 |
)
48 | }
49 | }
50 |
51 | export default Content
52 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceTabs/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import {Tab, Tabs} from './components/draggable-tab'
5 |
6 | class InstanceTabs extends React.Component {
7 | constructor() {
8 | super()
9 | this.style = 'block'
10 | }
11 |
12 | render() {
13 | const {instances, activeInstanceKey, onCreateInstance, onSelectInstance,
14 | onDelInstance, onMoveInstance} = this.props
15 |
16 | const style = instances.count() === 1 ? 'none' : 'block'
17 | if (this.style !== style) {
18 | this.style = style
19 | setTimeout(() => $(window).trigger('resize'), 0)
20 | }
21 |
22 | return (
23 | {
25 | if (!$('.Modal').length) {
26 | onCreateInstance()
27 | }
28 | }}
29 | onTabSelect={key => {
30 | if (!$('.Modal').length) {
31 | onSelectInstance(key)
32 | }
33 | }}
34 | onTabClose={key => {
35 | if (!$('.Modal').length) {
36 | onDelInstance(key)
37 | }
38 | }}
39 | onTabPositionChange={onMoveInstance}
40 | selectedTab={activeInstanceKey}
41 | tabs={instances.map(instance => ).toJS()}
42 | />
43 |
)
44 | }
45 | }
46 |
47 | export default InstanceTabs
48 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path')
4 |
5 | module.exports = {
6 | entry: {
7 | main: './client/windows/MainWindow/entry.jsx',
8 | patternManager: './client/windows/PatternManagerWindow/entry.jsx'
9 | },
10 | node: {
11 | Buffer: false,
12 | buffer: false
13 | },
14 | output: {
15 | filename: '[name].js'
16 | },
17 | module: {
18 | loaders: [{
19 | test: /\.jsx$/,
20 | exclude: /node_modules/,
21 | loader: 'jsx-loader?harmony!babel?stage=0&ignore=buffer'
22 | }, {
23 | test: /\.js$/,
24 | exclude: /node_modules/,
25 | loader: 'babel?stage=0&ignore=buffer'
26 | }, {
27 | test: /\.scss$/,
28 | loader: 'style!css!sass'
29 | }, {
30 | test: /\.css$/,
31 | loader: 'style!css'
32 | }, {
33 | test: /\.(png|jpg)$/,
34 | loader: "url-loader"
35 | }]
36 | },
37 | externals: {
38 | 'ioredis': 'require("ioredis")',
39 | 'electron': 'require("electron")',
40 | 'redis-commands': 'require("redis-commands")',
41 | 'ssh2': 'require("ssh2")',
42 | 'net': 'require("net")',
43 | 'remote': 'require("remote")',
44 | 'shell': 'require("shell")',
45 | 'app': 'require("app")',
46 | 'ipc': 'require("ipc")',
47 | 'fs': 'require("fs")',
48 | 'buffer': 'require("buffer")',
49 | 'system': '{}',
50 | 'file': '{}'
51 | },
52 | resolve: {
53 | alias: {
54 | Redux: path.resolve(__dirname, 'client/redux/'),
55 | Utils: path.resolve(__dirname, 'client/utils/'),
56 | },
57 | extensions: ['', '.js', '.jsx']
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/client/photon/template-app/app.js:
--------------------------------------------------------------------------------
1 | var app = require('app'); // Module to control application life.
2 | var BrowserWindow = require('browser-window'); // Module to create native browser window.
3 |
4 | // Keep a global reference of the window object, if you don't, the window will
5 | // be closed automatically when the JavaScript object is garbage collected.
6 | var mainWindow = null;
7 |
8 | // Quit when all windows are closed.
9 | app.on('window-all-closed', function() {
10 | // On OS X it is common for applications and their menu bar
11 | // to stay active until the user quits explicitly with Cmd + Q
12 | if (process.platform != 'darwin') {
13 | app.quit();
14 | }
15 | });
16 |
17 | // This method will be called when Electron has finished
18 | // initialization and is ready to create browser windows.
19 | app.on('ready', function() {
20 | // Create the browser window.
21 | mainWindow = new BrowserWindow({
22 | width: 600,
23 | height: 300,
24 | 'min-width': 500,
25 | 'min-height': 200,
26 | 'accept-first-mouse': true,
27 | 'title-bar-style': 'hidden'
28 | });
29 |
30 | // and load the index.html of the app.
31 | mainWindow.loadURL('file://' + __dirname + '/index.html');
32 |
33 | // Open the DevTools.
34 | //mainWindow.openDevTools();
35 |
36 | // Emitted when the window is closed.
37 | mainWindow.on('closed', function() {
38 | // Dereference the window object, usually you would store windows
39 | // in an array if your app supports multi windows, this is the time
40 | // when you should delete the corresponding element.
41 | mainWindow = null;
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/server/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const co = require('co');
4 | const {app, Menu, ipcMain} = require('electron');
5 | const windowManager = require('./windowManager');
6 | const menu = require('./menu');
7 | const tools = require('./tools');
8 |
9 | // Keep a global reference of the window object, if you don't, the window will
10 | // be closed automatically when the javascript object is GCed.
11 | // single instance
12 | const shouldQuit = app.makeSingleInstance((commandLine, workingDirectory) => {
13 | // Someone tried to run a second instance, we should focus our window.
14 | var current = windowManager.pickOne;
15 | if(current) {
16 | current.show();
17 | if (current.isMinimized()) {
18 | current.restore();
19 | }
20 | current.focus();
21 | }
22 | })
23 |
24 | if (shouldQuit) {
25 | app.quit()
26 | }
27 |
28 | co(function*(){
29 | if(tools.shouldRelaunch()) {
30 | yield tools.delFavo();
31 | app.relaunch({args: tools.args()});
32 | app.exit(0);
33 | }
34 |
35 | ipcMain.on('create patternManager', function (event, arg) {
36 | windowManager.create('patternManager', arg);
37 | });
38 |
39 | ipcMain.on('dispatch', function (event, action, arg) {
40 | windowManager.dispatch(action, arg);
41 | });
42 |
43 | // Quit when all windows are closed.
44 | app.on('window-all-closed', function () {
45 | if (process.platform !== 'darwin') {
46 | app.quit();
47 | }
48 | });
49 |
50 | app.on('activate', function (e, hasVisibleWindows) {
51 | if (!hasVisibleWindows) {
52 | windowManager.create();
53 | }
54 | });
55 |
56 | // This method will be called when Electron has finished
57 | // initialization and is ready to create browser windows.
58 | app.on('ready', function () {
59 | Menu.setApplicationMenu(menu);
60 | windowManager.create();
61 | });
62 | });
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/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 './components/StringContent'
7 | import ListContent from './components/ListContent'
8 | import SetContent from './components/SetContent'
9 | import HashContent from './components/HashContent'
10 | import ZSetContent from './components/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/KeyBrowser/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import {List} from 'immutable'
5 | import PatternList from './components/PatternList'
6 | import KeyList from './components/KeyList'
7 | import Footer from './components/Footer'
8 |
9 | class KeyBrowser extends React.Component {
10 | constructor(props) {
11 | super()
12 | this.footerHeight = 66
13 |
14 | this.state = {pattern: props.pattern}
15 | }
16 |
17 | componentWillReceiveProps(nextProps) {
18 | if (nextProps.pattern !== this.props.pattern) {
19 | this.setState({pattern: nextProps.pattern})
20 | }
21 | }
22 |
23 | render() {
24 | const {patterns, connectionKey} = this.props
25 | return (
26 |
{
33 | this.setState({pattern})
34 | }}
35 | />
36 | this.props.onSelectKey(key)}
45 | />
46 |
51 | )
52 | }
53 | }
54 |
55 | export default KeyBrowser
56 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/KeyBrowser/components/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 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/KeyBrowser/components/KeyList/index.scss:
--------------------------------------------------------------------------------
1 | .context-menu-item.context-menu-hover {
2 | background: #116cd6;
3 | color: #fff;
4 | }
5 |
6 | .pattern-table {
7 | position: relative;
8 | overflow: hidden;
9 | &:focus {
10 | outline: 0;
11 | }
12 |
13 | footer {
14 | height: 24px;
15 | }
16 | }
17 |
18 | .public_fixedDataTable_main {
19 | // border-bottom-color: transparent;
20 | }
21 |
22 | .public_fixedDataTable_bottomShadow {
23 | display: none;
24 | }
25 |
26 | .key-type {
27 | margin: 4px 0 0;
28 | padding: 0 !important;
29 | text-transform: uppercase;
30 | width: 32px;
31 | height: 16px;
32 | font-size: 11px !important;
33 | line-height: 17px !important;
34 | display: block;
35 | text-align: center;
36 | background: #60d4ca;
37 | color: #fff;
38 |
39 | &.str { background: #5dc936; }
40 | &.list { background: #fca32a; }
41 | &.hash { background: #b865d0; }
42 | &.zset { background: #fa5049; }
43 | &.set { background: #239ff2; }
44 | }
45 |
46 | .public_fixedDataTable_header, .public_fixedDataTableRow_main.is-loading {
47 | .public_fixedDataTableCell_main {
48 | font-family: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif !important;
49 | }
50 | }
51 |
52 | .public_fixedDataTableCell_cellContent {
53 | padding: 0;
54 | }
55 |
56 | .public_fixedDataTableCell_main {
57 | font-family: Consolas, monospace;
58 | font-size: 12px;
59 | line-height: 24px;
60 | padding: 0 8px;
61 | }
62 |
63 | .public_fixedDataTableRow_main {
64 | color: #606061;
65 | }
66 |
67 | :focus .public_fixedDataTableRow_main.is-selected {
68 | background: #116cd6;
69 | color: #fff;
70 |
71 | .public_fixedDataTableCell_main {
72 | background: transparent;
73 | }
74 | }
75 |
76 | .public_fixedDataTableRow_main.is-selected {
77 | background: #dcdcdc;
78 |
79 | .public_fixedDataTableCell_main {
80 | background: transparent;
81 | }
82 | }
83 |
84 | .public_fixedDataTableCell_main {
85 | border: none;
86 | }
87 |
88 | .public_fixedDataTable_main {
89 | border-right: none;
90 | border-left-color: transparent;
91 | }
92 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/ContentEditable/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | require('./index.scss')
5 |
6 | export default class ContentEditable extends React.Component {
7 | constructor() {
8 | super()
9 | }
10 |
11 | render() {
12 | const {html, enabled, ...props} = this.props
13 | return (
16 |
24 |
)
25 | }
26 |
27 | shouldComponentUpdate(nextProps) {
28 | return nextProps.html !== this.props.html || // ReactDOM.findDOMNode(this.refs.text).innerHTML ||
29 | nextProps.enabled !== this.props.enabled
30 | }
31 |
32 | componentDidMount() {
33 | if (this.props.enabled) {
34 | ReactDOM.findDOMNode(this.refs.text).focus()
35 | }
36 | }
37 |
38 | componentDidUpdate() {
39 | const node = ReactDOM.findDOMNode(this.refs.text)
40 | if (this.props.html !== node.innerHTML) {
41 | node.innerHTML = this.props.html
42 | }
43 | if (this.props.enabled) {
44 | const range = document.createRange()
45 | range.selectNodeContents(node)
46 | const sel = window.getSelection()
47 | sel.removeAllRanges()
48 | sel.addRange(range)
49 | }
50 | }
51 |
52 | handleKeyDown(evt) {
53 | if (evt.keyCode === 13) {
54 | ReactDOM.findDOMNode(this.refs.text).blur()
55 | evt.preventDefault()
56 | evt.stopPropagation()
57 | return
58 | }
59 | if (evt.keyCode === 27) {
60 | this.props.onChange(this.props.html)
61 | evt.preventDefault()
62 | evt.stopPropagation()
63 | }
64 | }
65 |
66 | handleChange(evt) {
67 | const html = ReactDOM.findDOMNode(this.refs.text).innerHTML
68 | if (html !== this.lastHtml) {
69 | evt.target = {value: html}
70 | }
71 | this.lastHtml = html
72 | }
73 |
74 | handleSubmit() {
75 | this.props.onChange(ReactDOM.findDOMNode(this.refs.text).textContent)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/ConnectionSelectorContainer/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {PureComponent} from 'react'
4 | import {Provider, connect} from 'react-redux'
5 | import Favorite from './components/Favorite'
6 | import Config from './components/Config'
7 | import store from 'Redux/store'
8 | import {connectToRedis} from 'Redux/actions'
9 | import {removeFavorite, updateFavorite, createFavorite, reorderFavorites} from 'Redux/actions'
10 |
11 | class ConnectionSelector extends PureComponent {
12 | constructor() {
13 | super()
14 | this.state = {connect: false, key: null}
15 | }
16 |
17 | handleSelectFavorite(connect, key) {
18 | this.setState({connect, key})
19 | }
20 |
21 | render() {
22 | const selectedFavorite = this.state.key && this.props.favorites.find(item => item.get('key') === this.state.key)
23 | return (
24 |
35 |
36 | {
42 | this.props.updateFavorite(selectedFavorite.get('key'), data)
43 | }}
44 | onDuplicate={this.props.createFavorite}
45 | />
46 |
47 |
)
48 | }
49 | }
50 |
51 | function mapStateToProps(state, {instance}) {
52 | return {
53 | favorites: state.favorites,
54 | connectStatus: instance.get('connectStatus')
55 | }
56 | }
57 | const mapDispatchToProps = {
58 | updateFavorite,
59 | createFavorite,
60 | connectToRedis,
61 | reorderFavorites,
62 | removeFavorite
63 | }
64 |
65 | export default connect(mapStateToProps, mapDispatchToProps)(ConnectionSelector)
66 |
--------------------------------------------------------------------------------
/server/windowManager.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {app, BrowserWindow} = require('electron');
4 | const EventEmitter = require('events');
5 |
6 | class WindowManager extends EventEmitter {
7 | constructor() {
8 | super();
9 | this.windows = new Set();
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 | get pickOne() {
19 | if(BrowserWindow.getAllWindows().length > 0) {
20 | if (BrowserWindow.getFocusedWindow()) {
21 | return BrowserWindow.getFocusedWindow();
22 | } else {
23 | return BrowserWindow.getAllWindows()[0];
24 | }
25 | } else {
26 | return null;
27 | }
28 | }
29 |
30 | create(type, arg) {
31 | if (!type) {
32 | type = 'main';
33 | }
34 | const option = {
35 | backgroundColor: '#ececec'
36 | };
37 | if (type === 'main') {
38 | option.width = 960;
39 | option.height = 600;
40 | option.show = true;
41 | option.minWidth = 840;
42 | option.minHeight = 400;
43 | } else if (type === 'patternManager') {
44 | option.width = 600;
45 | option.height = 300;
46 | option.title = 'Manage Patterns';
47 | option.resizable = true;
48 | option.fullscreen = false;
49 | }
50 |
51 | const newWindow = new BrowserWindow(option);
52 | if (!option.show) {
53 | newWindow.once('ready-to-show', () => {
54 | newWindow.show();
55 | })
56 | }
57 | newWindow.loadURL(`file://${__dirname}/windows/${type}.html${arg ? '?arg=' + arg : ''}`);
58 |
59 | this._register(newWindow);
60 |
61 | // for debug
62 | //newWindow.webContents.openDevTools();
63 |
64 | return newWindow;
65 | }
66 |
67 | _register(win) {
68 | this.windows.add(win);
69 | win.on('closed', () => {
70 | this.windows.delete(win);
71 | if (!BrowserWindow.getFocusedWindow()) {
72 | this.emit('blur');
73 | }
74 | });
75 | this.emit('focus');
76 | }
77 |
78 | dispatch(action, args) {
79 | this.windows.forEach(win => {
80 | if (win && win.webContents) {
81 | win.webContents.send('action', action, args);
82 | }
83 | });
84 | }
85 | }
86 |
87 | module.exports = new WindowManager();
88 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React, {PureComponent} from 'react'
4 | import ConnectionSelectorContainer from './components/ConnectionSelectorContainer'
5 | import DatabaseContainer from './components/DatabaseContainer'
6 | import Modal from '../../components/InstanceContent/components/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/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 | class Footer extends React.PureComponent {
15 | constructor() {
16 | super()
17 | this.resetState(true)
18 | }
19 |
20 | resetState(sync) {
21 | const values = {
22 | ttl: null,
23 | encoding: null,
24 | size: null
25 | }
26 | if (sync) {
27 | this.state = values
28 | } else {
29 | this.setState(values)
30 | }
31 | }
32 |
33 | init(keyName, keyType) {
34 | if (!keyType && keyType !== 'none') {
35 | this.resetState()
36 | return
37 | }
38 | const pipeline = this.props.redis.pipeline()
39 | pipeline.pttl(keyName)
40 | pipeline.object('ENCODING', keyName)
41 |
42 | let sizeUnit = 'Members'
43 | switch (keyType) {
44 | case 'string': pipeline.strlen(keyName); sizeUnit = 'Bytes'; break
45 | case 'hash': pipeline.hlen(keyName); break
46 | case 'list': pipeline.llen(keyName); break
47 | case 'set': pipeline.scard(keyName); break
48 | case 'zset': pipeline.zcard(keyName); break
49 | }
50 |
51 | pipeline.exec((err, [[err1, pttl], [err2, encoding], res3]) => {
52 | this.setState({
53 | encoding: encoding ? `Encoding: ${encoding}` : '',
54 | ttl: pttl >= 0 ? `TTL: ${humanFormat(pttl, {scale: timeScale}).replace(' ', '')}` : null,
55 | size: (res3 && res3[1]) ? `${sizeUnit}: ${res3[1]}` : null
56 | })
57 | })
58 | }
59 |
60 | componentDidMount() {
61 | this.init(this.props.keyName, this.props.keyType)
62 | }
63 |
64 | componentWillReceiveProps(nextProps) {
65 | if (nextProps.keyName !== this.props.keyName ||
66 | nextProps.keyType !== this.props.keyType ||
67 | nextProps.version !== this.props.version) {
68 | this.init(nextProps.keyName, nextProps.keyType)
69 | }
70 | }
71 |
72 | render() {
73 | const desc = ['size', 'encoding', 'ttl']
74 | .map(key => ({key, value: this.state[key]}))
75 | .filter(item => typeof item.value === 'string')
76 | return ()
84 | }
85 | }
86 |
87 | export default Footer
88 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/KeyBrowser/components/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import TabBar from './components/TabBar'
5 | import KeyContent from './components/KeyContent'
6 | import Terminal from './components/Terminal'
7 | import Config from './components/Config'
8 | import Footer from './components/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 |
55 | {
62 | this.setState({version: this.state.version + 1})
63 | }}
64 | />
65 |
72 |
78 |
84 |
)
85 | }
86 | }
87 |
88 | export default Content
89 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/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 './components/KeyBrowser'
7 | import Content from './components/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() - $('.tab-group').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() - $('.tab-group').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({key, version: this.state.version + 1})}
65 | onCreateKey={this.handleCreateKey.bind(this)}
66 | db={this.state.db}
67 | onDatabaseChange={db => this.setState({db})}
68 | onKeyMetaChange={() => this.setState({metaVersion: this.state.metaVersion + 1})}
69 | />
70 | this.setState({db})}
79 | />
80 | )
81 | }
82 | }
83 |
84 | function mapStateToProps(state, {instance}) {
85 | return {
86 | patterns: state.patterns,
87 | redis: instance.get('redis'),
88 | connectionKey: instance.get('connectionKey')
89 | }
90 | }
91 |
92 | export default connect(mapStateToProps)(Database)
93 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/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 |
--------------------------------------------------------------------------------
/client/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 './components/InstanceTabs'
7 | import InstanceContent from './components/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "medis",
3 | "description": "GUI for Redis",
4 | "productName": "Medis",
5 | "version": "0.6.1",
6 | "electronVersion": "1.4.15",
7 | "license": "MIT",
8 | "author": "luin (http://zihua.li)",
9 | "main": "server/main.js",
10 | "scripts": {
11 | "build": "webpack --progress --colors",
12 | "watch": "npm run build -- --watch",
13 | "electron": "electron .",
14 | "lint": "xo client/**/*.{js,jsx}",
15 | "pack": "NODE_ENV=production npm run build -- -p --config webpack.production.config.js && node bin/pack.js",
16 | "release": "NODE_ENV=production ./bin/release"
17 | },
18 | "xo": {
19 | "extends": "xo-react/space",
20 | "semicolon": false,
21 | "ignore": [
22 | "client/vendors/**",
23 | "client/photon/**"
24 | ],
25 | "envs": [
26 | "browser"
27 | ],
28 | "globals": [
29 | "showModal"
30 | ],
31 | "rules": {
32 | "indent": [
33 | "error",
34 | 2
35 | ],
36 | "unicorn/filename-case": "off",
37 | "operator-linebreak": [
38 | "error",
39 | "after",
40 | {
41 | "overrides": {
42 | "?": "before",
43 | ":": "before"
44 | }
45 | }
46 | ],
47 | "new-cap": "off",
48 | "import/no-unassigned-import": "off",
49 | "import/default": "off",
50 | "import/prefer-default-export": "off"
51 | }
52 | },
53 | "repository": {
54 | "type": "git",
55 | "url": "git@github.com:classfellow/medis.git"
56 | },
57 | "dependencies": {
58 | "bluebird": "^3.5.0",
59 | "co": "^4.6.0",
60 | "del": "^3.0.0",
61 | "fixed-data-table-contextmenu": "^1.6.5",
62 | "ioredis": "^2.4.2",
63 | "jquery": "^2.1.4",
64 | "lodash": "^3.10.1",
65 | "redis-commands": "^1.0.1",
66 | "ssh2": "^0.5.4"
67 | },
68 | "devDependencies": {
69 | "asar": "^0.8.3",
70 | "babel-core": "^5.8.25",
71 | "babel-eslint": "^4.1.6",
72 | "babel-loader": "^5.3.2",
73 | "codemirror": "^5.25.2",
74 | "conventional-github-releaser": "^0.5.3",
75 | "css-loader": "^0.19.0",
76 | "cz-conventional-changelog": "^1.1.5",
77 | "electron": "1.4.15",
78 | "electron-osx-sign": "^0.4.4",
79 | "electron-packager": "^8.6.0",
80 | "eslint-config-xo": "^0.18.1",
81 | "eslint-config-xo-react": "^0.11.1",
82 | "eslint-plugin-react": "^6.10.3",
83 | "extract-text-webpack-plugin": "^1.0.1",
84 | "github": "^0.2.4",
85 | "human-format": "^0.5.0",
86 | "immutable": "^3.8.1",
87 | "json-editor": "^0.7.23",
88 | "jsonlint": "^1.6.2",
89 | "jsx-loader": "^0.13.2",
90 | "lint": "^1.1.2",
91 | "minimatch": "^3.0.4",
92 | "msgpack5": "^3.3.0",
93 | "node-sass": "^3.3.3",
94 | "prop-types": "^15.5.10",
95 | "react": "^15.5.4",
96 | "react-addons-css-transition-group": "^15.5.2",
97 | "react-codemirror": "^0.3.0",
98 | "react-document-title": "^2.0.1",
99 | "react-dom": "^15.5.4",
100 | "react-draggable": "^2.2.6",
101 | "react-redux": "^5.0.4",
102 | "react-split-pane": "^0.1.63",
103 | "redis-splitargs": "^1.0.0",
104 | "redux": "^3.6.0",
105 | "redux-actions": "^2.0.3",
106 | "reselect": "^3.0.1",
107 | "sass-loader": "^3.0.0",
108 | "sortablejs": "^1.4.1",
109 | "style-loader": "^0.12.4",
110 | "url-loader": "^0.5.7",
111 | "webpack": "^1.12.2",
112 | "webpack-dev-server": "^1.12.0",
113 | "xo": "^0.18.1"
114 | },
115 | "config": {
116 | "commitizen": {
117 | "path": "./node_modules/cz-conventional-changelog"
118 | }
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/KeyBrowser/components/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 | render() {
84 | const db = `db${this.props.db}`
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 ()
121 | }
122 | }
123 |
124 | export default Footer
125 |
--------------------------------------------------------------------------------
/client/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 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/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 | constructor() {
9 | super()
10 | }
11 |
12 | handleSubmit() {
13 | if (this.editor) {
14 | const errors = this.editor.validate()
15 | if (errors.length) {
16 | $('.ui-state-error', ReactDOM.findDOMNode(this.refs.form)).css('opacity', 1)
17 | return
18 | }
19 | this.props.onSubmit(this.editor.getValue())
20 | } else {
21 | this.props.onSubmit(1)
22 | }
23 | }
24 |
25 | handleCancel() {
26 | this.props.onCancel()
27 | }
28 |
29 | componentDidMount() {
30 | if (this.props.form) {
31 | this.editor = new JSONEditor(ReactDOM.findDOMNode(this.refs.form), {
32 | disable_array_add: true,
33 | disable_array_delete: true,
34 | disable_array_reorder: true,
35 | disable_collapse: true,
36 | disable_edit_json: true,
37 | disable_properties: true,
38 | required_by_default: true,
39 | schema: this.props.form,
40 | show_errors: 'always',
41 | theme: 'jqueryui'
42 | })
43 |
44 | $('.row input, .row select', ReactDOM.findDOMNode(this.refs.form)).first().focus()
45 | } else {
46 | $('.nt-button', ReactDOM.findDOMNode(this)).first().focus()
47 | }
48 | }
49 |
50 | handleKeyDown(evt) {
51 | if (evt.keyCode === 9) {
52 | const $all = $('.row input, .row select, .nt-button', ReactDOM.findDOMNode(this))
53 | const focused = $(':focus')[0]
54 | let i
55 | for (i = 0; i < $all.length - 1; ++i) {
56 | if ($all[i] != focused) {
57 | continue
58 | }
59 | $all[i + 1].focus()
60 | break
61 | }
62 | // Must have been focused on the last one or none of them.
63 | if (i == $all.length - 1) {
64 | $all[0].focus()
65 | }
66 | evt.stopPropagation()
67 | evt.preventDefault()
68 | return
69 | }
70 | if (evt.keyCode === 27) {
71 | this.handleCancel()
72 | evt.stopPropagation()
73 | evt.preventDefault()
74 | return
75 | }
76 | if (evt.keyCode === 13) {
77 | const node = ReactDOM.findDOMNode(this.props.form ? this.refs.cancel : this.refs.submit)
78 | node.focus()
79 | setTimeout(() => {
80 | node.click()
81 | }, 10)
82 | evt.stopPropagation()
83 | evt.preventDefault()
84 | }
85 | }
86 |
87 | render() {
88 | return (
93 |
94 | {
95 | this.props.title &&
96 | {this.props.title}
97 |
98 | }
99 |
100 | {!this.props.form &&
}
101 | {this.props.content}
102 |
103 |
104 |
105 |
110 |
115 |
116 |
117 |
)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/ConnectionSelectorContainer/components/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 |
96 |
124 |
)
125 | }
126 |
127 | componentWillUnmount() {
128 | this.sortable.destroy()
129 | }
130 | }
131 |
132 | export default Favorite
133 |
--------------------------------------------------------------------------------
/client/windows/PatternManagerWindow/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import {connect} from 'react-redux'
4 | import {createPattern, reorderPatterns, updatePattern, removePattern} from 'Redux/actions'
5 | import {List} from 'immutable'
6 |
7 | require('./app.scss')
8 |
9 | const connectionKey = getParameterByName('arg')
10 |
11 | class App extends React.Component {
12 | constructor(props, context) {
13 | super(props, context)
14 | this.state = {index: 0}
15 | }
16 |
17 | handleChange(property, e) {
18 | this.setState({[property]: e.target.value})
19 | }
20 |
21 | render() {
22 | const {patterns, createPattern, removePattern} = this.props
23 | const activePattern = patterns.get(this.state.index)
24 | return (
25 |
26 |
37 |
55 |
56 |
61 |
62 |
63 |
69 |
70 |
71 |
72 |
78 |
79 |
80 |
90 |
91 |
92 |
)
93 | }
94 | }
95 |
96 | function mapStateToProps(state) {
97 | return {
98 | patterns: state.patterns.get(connectionKey, List())
99 | }
100 | }
101 |
102 | const mapDispatchToProps = {
103 | updatePattern,
104 | reorderPatterns,
105 | createPattern,
106 | removePattern
107 | }
108 |
109 | export default connect(mapStateToProps, mapDispatchToProps)(App)
110 |
111 | function getParameterByName(name) {
112 | name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
113 | const regex = new RegExp('[\\?&]' + name + '=([^]*)')
114 | const results = regex.exec(location.search)
115 | return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '))
116 | }
117 |
--------------------------------------------------------------------------------
/client/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: 1px;
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 | box-shadow: 0 0 0 3.5px #93c2f3;
49 | }
50 |
51 | &:placeholder {
52 | color: #c0c0c0;
53 | }
54 |
55 | &[disabled] {
56 | background: #f8f8f8;
57 | }
58 | }
59 |
60 | input[type="radio"],
61 | input[type="checkbox"] {
62 | line-height: normal;
63 | }
64 |
65 | &.nt-form-row--vertical {
66 | overflow: visible;
67 | label {
68 | float: none;
69 | display: block;
70 | text-align: left;
71 | }
72 | input[type="text"], input[type="password"], textarea {
73 | margin-left: 0;
74 | width: 100%;
75 | }
76 | }
77 | }
78 |
79 | .nt-button {
80 | cursor: default;
81 | background-color: #ffffff;
82 | outline: none;
83 | border-width: 1px;
84 | border-style: solid;
85 | border-radius: 5px;
86 | border-top-color: #c8c8c8;
87 | border-bottom-color: #acacac;
88 | border-left-color: #c2c2c2;
89 | border-right-color: #c2c2c2;
90 | box-shadow: 0 1px rgba(0, 0, 0, .039);
91 | padding-top: 0;
92 | padding-bottom: 0;
93 | padding-left: 20px;
94 | padding-right: 20px;
95 | line-height: 19px;
96 | font-size: 13px;
97 |
98 | &:active {
99 | background-image: -webkit-linear-gradient(top, #4c98fe 0%, #0564e3 100%);
100 | border-top-color: #247fff;
101 | border-bottom-color: #003ddb;
102 | border-left-color: #125eed;
103 | border-right-color: #125eed;
104 | color: rgba(255, 255, 255, .9);
105 | }
106 |
107 | margin-right: 10px;
108 | &:last-child {
109 | margin-right: 0;
110 | }
111 | }
112 |
113 | .nt-button--primary {
114 | background-image: -webkit-linear-gradient(top, #6cb3fa 0%, #087eff 100%);
115 | border-top-color: #4ca2f9;
116 | border-bottom-color: #015cff;
117 | border-left-color: #267ffc;
118 | border-right-color: #267ffc;
119 | color: rgba(255, 255, 255, .9);
120 |
121 | &:active {
122 | background-image: -webkit-linear-gradient(top, #4c98fe 0%, #0564e3 100%);
123 | border-top-color: #247fff;
124 | border-bottom-color: #003ddb;
125 | border-left-color: #125eed;
126 | border-right-color: #125eed;
127 | color: rgba(255, 255, 255, .9);
128 | }
129 | }
130 |
131 | .nt-button:focus {
132 | outline: none;
133 | box-shadow: 0 0 0 3.5px #93c2f3;
134 | }
135 |
136 | .nt-button[disabled], .nt-button--disabled {
137 | background: #f1f1f1;
138 | border-color: #d0d0d0;
139 | color: #b1b1b1;
140 | }
141 |
142 | .nt-button-group {
143 | text-align: center;
144 |
145 | .nt-button {
146 | display: inline-block;
147 | }
148 | &.nt-button-group--pull-right {
149 | text-align: right;
150 | }
151 | }
152 |
153 | .context-menu-list, .context-menu-item {
154 | background: #f0f0f0;
155 | }
156 |
157 | .context-menu-list {
158 | box-shadow: 0 2px 10px rgba(0, 0, 0, .2);
159 | }
160 |
161 | .context-menu-separator {
162 | border-bottom: 2px solid #dfdfdf;
163 | }
164 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Medis for Windows
2 |
3 | 
4 |
5 | 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).
6 |
7 | [](http://commitizen.github.io/cz-cli/)
8 | [](https://gitter.im/luin/medis?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
9 |
10 | Medis starts with all the basic features you need:
11 |
12 | * Keys viewing/editing
13 | * SSH Tunnel for connecting with remote servers
14 | * Terminal for executing custom commands
15 | * Config viewing/editing
16 |
17 | It also supports many advanced features:
18 |
19 | * JSON/MessagePack format viewing/editing and built-in highlighting/validator
20 | * Working with millions keys and key members without blocking the redis server
21 | * Pattern manager for easy selecting a sub group of keys.
22 |
23 | **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 3.0 and 2.6 is a very old version, Medis doesn't support it.
24 |
25 | ## Download Medis on Windows
26 |
27 | You can download compiled installer of Medis for Windows from the below page
28 | [download page](https://github.com/classfellow/medis/releases/tag/win)
29 |
30 | ## Download Medis on Mac
31 |
32 | You can download compiled versions of Medis for Mac OS X from [the release page](https://github.com/luin/medis/releases).
33 |
34 | ## Running Locally
35 |
36 | 1. Install dependencies
37 |
38 | $ npm install
39 |
40 | 2. Compile assets:
41 |
42 | $ npm run build
43 |
44 | 3. Run with Electron:
45 |
46 | $ npm run electron
47 |
48 | ## Connect to Heroku
49 | 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:
50 |
51 | ```shell
52 | $ heroku redis:credentials --app YOUR_APP
53 | redis://x:PASSWORD@HOST:PORT
54 | ```
55 |
56 | And then input `HOST`, `PORT` and `PASSWORD` to the connection tab.
57 |
58 | ## I Love This. How do I Help?
59 |
60 | * Simply star this repository :-)
61 | * Help us spread the world on Facebook and Twitter
62 | * Contribute Code! We're developers! (See Roadmap below)
63 | * 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://itunes.apple.com/app/medis-gui-for-redis/id1063631769)
64 |
65 | ## Roadmap
66 |
67 | * Windows and Linux version (with electron-packager)
68 | * Support for SaaS Redis services
69 | * Lua script editor
70 | * Cluster management
71 | * GEO keys supporting
72 |
73 | ## Contributors
74 |  luin |  kvnsmth |  dpde |  ogasawaraShinnosuke |  naholyr |
 hlobil |  Janpot |  Youjia |
75 |
76 | ## License
77 |
78 | MIT
79 |
--------------------------------------------------------------------------------
/server/menu.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {app, Menu, shell} = require('electron');
4 | const windowManager = require('./windowManager');
5 | const tools = require('./tools');
6 |
7 | const menuTemplate = [{
8 | label: 'File',
9 | submenu: [{
10 | label: 'New Connection Window',
11 | accelerator: 'CmdOrCtrl+N',
12 | click() {
13 | windowManager.create();
14 | }
15 | }, {
16 | label: 'New Connection Tab',
17 | accelerator: 'CmdOrCtrl+T',
18 | click() {
19 | windowManager.current.webContents.send('action', 'createInstance');
20 | }
21 | }, {
22 | type: 'separator'
23 | }, {
24 | label: 'Close Window',
25 | accelerator: 'Shift+CmdOrCtrl+W',
26 | click() {
27 | windowManager.current.close();
28 | }
29 | }, {
30 | label: 'Close Tab',
31 | accelerator: 'CmdOrCtrl+W',
32 | click() {
33 | windowManager.current.webContents.send('action', 'delInstance');
34 | }
35 | }]
36 | }, {
37 | label: 'Edit',
38 | submenu: [{
39 | label: 'Undo',
40 | accelerator: 'CmdOrCtrl+Z',
41 | role: 'undo'
42 | }, {
43 | label: 'Redo',
44 | accelerator: 'Shift+CmdOrCtrl+Z',
45 | role: 'redo'
46 | }, {
47 | type: 'separator'
48 | }, {
49 | label: 'Cut',
50 | accelerator: 'CmdOrCtrl+X',
51 | role: 'cut'
52 | }, {
53 | label: 'Copy',
54 | accelerator: 'CmdOrCtrl+C',
55 | role: 'copy'
56 | }, {
57 | label: 'Paste',
58 | accelerator: 'CmdOrCtrl+V',
59 | role: 'paste'
60 | }, {
61 | label: 'Select All',
62 | accelerator: 'CmdOrCtrl+A',
63 | role: 'selectall'
64 | }]
65 | }, {
66 | label: 'View',
67 | submenu: [{
68 | label: 'Reload',
69 | accelerator: 'CmdOrCtrl+R',
70 | click(item, focusedWindow) {
71 | if (focusedWindow) {
72 | focusedWindow.reload();
73 | }
74 | }
75 | }, {
76 | label: 'Toggle Full Screen',
77 | accelerator: (function () {
78 | if (process.platform === 'darwin') {
79 | return 'Ctrl+Command+F';
80 | }
81 | return 'F11';
82 | })(),
83 | click(item, focusedWindow) {
84 | if (focusedWindow) {
85 | focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
86 | }
87 | }
88 | }, {
89 | label: 'Toggle Developer Tools',
90 | accelerator: (function () {
91 | if (process.platform === 'darwin') {
92 | return 'Alt+Command+I';
93 | }
94 | return 'Ctrl+Shift+I';
95 | })(),
96 | click(item, focusedWindow) {
97 | if (focusedWindow) {
98 | focusedWindow.toggleDevTools();
99 | }
100 | }
101 | }]
102 | }, {
103 | label: 'Window',
104 | role: 'window',
105 | submenu: [{
106 | label: 'Minimize',
107 | accelerator: 'CmdOrCtrl+M',
108 | role: 'minimize'
109 | }, {
110 | label: 'Close',
111 | accelerator: 'CmdOrCtrl+W',
112 | role: 'close'
113 | }]
114 | },
115 | tools.label,
116 | {
117 | label: 'Help',
118 | role: 'help',
119 | submenu: [{
120 | label: 'Report an Issue...',
121 | click() {
122 | shell.openExternal('mailto:youjia@shimo.im');
123 | }
124 | }, {
125 | label: 'Mac Version',
126 | click() {
127 | shell.openExternal('http://getmedis.com');
128 | }
129 | }, {
130 | label: 'Windows Version',
131 | click() {
132 | shell.openExternal('http://yiss.bid/medis/readme');
133 | }
134 | }]
135 | }]
136 |
137 | let baseIndex = 0;
138 | if (process.platform === 'darwin') {
139 | baseIndex = 1;
140 | menuTemplate.unshift({
141 | label: app.getName(),
142 | submenu: [{
143 | label: 'About ' + app.getName(),
144 | role: 'about'
145 | }, {
146 | type: 'separator'
147 | }, {
148 | label: 'Services',
149 | role: 'services',
150 | submenu: []
151 | }, {
152 | type: 'separator'
153 | }, {
154 | label: 'Hide ' + app.getName(),
155 | accelerator: 'Command+H',
156 | role: 'hide'
157 | }, {
158 | label: 'Hide Others',
159 | accelerator: 'Command+Shift+H',
160 | role: 'hideothers'
161 | }, {
162 | label: 'Show All',
163 | role: 'unhide'
164 | }, {
165 | type: 'separator'
166 | }, {
167 | label: 'Quit',
168 | accelerator: 'Command+Q',
169 | click() {
170 | app.quit();
171 | }
172 | }]
173 | });
174 | }
175 |
176 | const menu = Menu.buildFromTemplate(menuTemplate);
177 |
178 | // if (process.env.NODE_ENV !== 'debug') {
179 | // menu.items[baseIndex + 2].submenu.items[0].visible = false;
180 | // menu.items[baseIndex + 2].submenu.items[2].visible = false;
181 | // }
182 |
183 | windowManager.on('blur', function () {
184 | menu.items[baseIndex + 0].submenu.items[3].enabled = false;
185 | menu.items[baseIndex + 0].submenu.items[4].enabled = false;
186 | });
187 |
188 | windowManager.on('focus', function () {
189 | menu.items[baseIndex + 0].submenu.items[3].enabled = true;
190 | menu.items[baseIndex + 0].submenu.items[4].enabled = true;
191 | });
192 |
193 | module.exports = menu;
194 |
--------------------------------------------------------------------------------
/client/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 | import _ from 'lodash';
8 |
9 | function getIndex(getState) {
10 | const {activeInstanceKey, instances} = getState()
11 | return instances.findIndex(instance => instance.get('key') === activeInstanceKey)
12 | }
13 |
14 | export const updateConnectStatus = createAction('UPDATE_CONNECT_STATUS', status => ({getState, next}) => {
15 | next({status, index: getIndex(getState)})
16 | })
17 |
18 | export const disconnect = createAction('DISCONNECT', () => ({getState, next}) => {
19 | next({index: getIndex(getState)})
20 | })
21 |
22 | export const connectToRedis = createAction('CONNECT', config => ({getState, dispatch, next}) => {
23 | let sshErrorThrown = false
24 | let redisErrorThrown = false
25 | let redisErrorMessage
26 |
27 | if (config.ssh) {
28 | dispatch(updateConnectStatus('SSH connecting...'))
29 |
30 | const conn = new Client();
31 | conn.on('ready', () => {
32 | const server = net.createServer(function (sock) {
33 | conn.forwardOut(sock.remoteAddress, sock.remotePort, config.host, config.port, (err, stream) => {
34 | if (err) {
35 | sock.end()
36 | } else {
37 | sock.pipe(stream).pipe(sock)
38 | }
39 | })
40 | }).listen(0, function () {
41 | handleRedis(config, { host: '127.0.0.1', port: server.address().port })
42 | })
43 | }).on('error', err => {
44 | sshErrorThrown = true;
45 | dispatch(disconnect());
46 | alert(`SSH Error: ${err.message}`);
47 | })
48 |
49 | try {
50 | const connectionConfig = {
51 | host: config.sshHost,
52 | port: config.sshPort || 22,
53 | username: config.sshUser
54 | }
55 | if (config.sshKey) {
56 | conn.connect(Object.assign(connectionConfig, {
57 | privateKey: config.sshKey,
58 | passphrase: config.sshKeyPassphrase
59 | }))
60 | } else {
61 | conn.connect(Object.assign(connectionConfig, {
62 | password: config.sshPassword
63 | }))
64 | }
65 | } catch (err) {
66 | dispatch(disconnect());
67 | alert(`SSH Error: ${err.message}`);
68 | }
69 | } else {
70 | handleRedis(config);
71 | }
72 |
73 | function handleRedis(config, override) {
74 | dispatch(updateConnectStatus('Redis connecting...'))
75 | if (config.ssl) {
76 | config.tls = {};
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(_.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 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "node": true,
5 | "browser": true,
6 | "es6": true
7 | },
8 | "plugins": [
9 | "react"
10 | ],
11 | "ecmaFeatures": {
12 | "jsx": true
13 | },
14 | "globals": {
15 | "$": true
16 | },
17 | "rules": {
18 | "no-var": 1,
19 | "no-const-assign": 2,
20 | "strict": [2, "global"],
21 |
22 | "comma-dangle": [1, "never"],
23 | "no-cond-assign": [1, "except-parens"],
24 | "no-console": 0,
25 | "no-constant-condition": 1,
26 | "no-control-regex": 1,
27 | "no-debugger": 1,
28 | "no-dupe-args": 1,
29 | "no-dupe-keys": 1,
30 | "no-duplicate-case": 0,
31 | "no-empty-character-class": 1,
32 | "no-empty": 1,
33 | "no-ex-assign": 1,
34 | "no-extra-boolean-cast": 1,
35 | "no-extra-parens": 0,
36 | "no-extra-semi": 1,
37 | "no-func-assign": 1,
38 | "no-inner-declarations": [1, "functions"],
39 | "no-invalid-regexp": 1,
40 | "no-irregular-whitespace": 1,
41 | "no-negated-in-lhs": 1,
42 | "no-obj-calls": 1,
43 | "no-regex-spaces": 1,
44 | "no-reserved-keys": 0,
45 | "no-sparse-arrays": 1,
46 | "no-unexpected-multiline": 1,
47 | "no-unreachable": 1,
48 | "use-isnan": 1,
49 | "valid-typeof": 1,
50 |
51 | "accessor-pairs": 0,
52 | "block-scoped-var": 0,
53 | "complexity": 0,
54 | "consistent-return": 0,
55 | "curly": [1, "all"],
56 | "default-case": 0,
57 | "dot-notation": [1, { "allowKeywords": true, "allowPattern": "" }],
58 | "dot-location": [1, "property"],
59 | "eqeqeq": 1,
60 | "guard-for-in": 0,
61 | "no-caller": 1,
62 | "no-div-regex": 1,
63 | "no-else-return": 1,
64 | "no-empty-label": 1,
65 | "no-eq-null": 0,
66 | "no-eval": 1,
67 | "no-extend-native": 1,
68 | "no-extra-bind": 1,
69 | "no-fallthrough": 0,
70 | "no-floating-decimal": 1,
71 | "no-implied-eval": 1,
72 | "no-iterator": 1,
73 | "no-labels": 1,
74 | "no-lone-blocks": 1,
75 | "no-loop-func": 1,
76 | "no-multi-spaces": 1,
77 | "no-multi-str": 1,
78 | "no-native-reassign": 1,
79 | "no-new-func": 1,
80 | "no-new-wrappers": 1,
81 | "no-new": 1,
82 | "no-octal-escape": 1,
83 | "no-octal": 1,
84 | "no-param-reassign": 0,
85 | "no-process-env": 0,
86 | "no-proto": 1,
87 | "no-redeclare": 1,
88 | "no-return-assign": 1,
89 | "no-script-url": 1,
90 | "no-self-compare": 1,
91 | "no-sequences": 1,
92 | "no-throw-literal": 1,
93 | "no-unused-expressions": 0,
94 | "no-void": 0,
95 | "no-warning-comments": [1, { "terms": ["todo", "tofix"], "location": "start" }],
96 | "no-with": 1,
97 | "radix": 1,
98 | "vars-on-top": 1,
99 | "wrap-iife": [1, "inside"],
100 | "yoda": [1, "never"],
101 |
102 | "no-catch-shadow": 0,
103 | "no-delete-var": 1,
104 | "no-label-var": 1,
105 | "no-shadow-restricted-names": 1,
106 | "no-shadow": 0,
107 | "no-undef-init": 1,
108 | "no-undef": 1,
109 | "no-unused-vars": [1, { "vars": "local", "args": "after-used" }],
110 | no-use-before-define: [1, "nofunc"],
111 |
112 | "handle-callback-err": 1,
113 | "no-mixed-requires": 1,
114 | "no-new-require": 1,
115 | "no-path-concat": 1,
116 | "no-process-exit": 1,
117 | "no-restricted-modules": [1, ""], // add any unwanted Node.js core modules
118 |
119 | "array-bracket-spacing": [1, "never"],
120 | "brace-style": [1],
121 | "comma-spacing": [1, { "before": false, "after": true }],
122 | "comma-style": [1, "last"],
123 | "computed-property-spacing": 0,
124 | "consistent-this": 0,
125 | "eol-last": 1,
126 | "func-style": 0,
127 | "indent": [1, 2],
128 | "key-spacing": [1, { "beforeColon": false, "afterColon": true }],
129 | "linebreak-style": 0,
130 | "max-nested-callbacks": [0, 3],
131 | "new-parens": 1,
132 | "newline-after-var": 0,
133 | "no-array-constructor": 1,
134 | "no-continue": 0,
135 | "no-inline-comments": 0,
136 | "no-lonely-if": 1,
137 | "no-mixed-spaces-and-tabs": 1,
138 | "no-multiple-empty-lines": [1, { "max": 1 }],
139 | "no-nested-ternary": 0,
140 | "no-new-object": 1,
141 | "no-spaced-func": 1,
142 | "no-ternary": 0,
143 | "no-trailing-spaces": 1,
144 | "no-underscore-dangle": 0,
145 | "object-curly-spacing": [1, "always"],
146 | "one-var": [1, "never"],
147 | "padded-blocks": [0, "never"],
148 | "quote-props": [0, "as-needed"],
149 | "quotes": [1, "single"],
150 | "semi-spacing": [1, { "before": false, "after": true }],
151 | "semi": [1, "always"],
152 | "sort-vars": 0,
153 | "space-after-keywords": 0,
154 | "space-before-blocks": [1, "always"],
155 | "space-before-function-paren": [1, { "anonymous": "always", "named": "never" }],
156 | "space-in-parens": [1, "never"],
157 | "space-infix-ops": 1,
158 | "space-return-throw-case": 1,
159 | "space-unary-ops": 0,
160 | "spaced-comment": [1, "always"],
161 | "wrap-regex": 1,
162 |
163 | "constructor-super": 1,
164 | "generator-star-spacing": [1, { "before": true, "after": false }],
165 | "no-this-before-super": 1,
166 | "no-var": 1,
167 | "object-shorthand": [1, "always"],
168 | "prefer-const": 1,
169 |
170 | "max-depth": [0, 3],
171 | "max-len": [1, 200, 2],
172 | "max-params": 0,
173 | "max-statements": 0,
174 | "no-bitwise": 1,
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceTabs/components/draggable-tab/components/Tabs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import _ from 'lodash';
4 | import PropTypes from 'prop-types';
5 | import React from 'react';
6 | import Draggable from 'react-draggable';
7 |
8 | class Tabs extends React.Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | const defaultState = this._tabStateFromProps(this.props);
13 | defaultState.selectedTab = this.props.selectedTab ? this.props.selectedTab :
14 | this.props.tabs ? this.props.tabs[0].key : '';
15 | this.state = defaultState;
16 |
17 | // Dom positons
18 | // do not save in state
19 | this.startPositions = [];
20 | }
21 |
22 | _tabStateFromProps(props) {
23 | const tabs = [];
24 | let idx = 0;
25 | React.Children.forEach(props.tabs, (tab) => {
26 | tabs[idx++] = tab;
27 | });
28 |
29 | return { tabs };
30 | }
31 |
32 | _getIndexOfTabByKey(key) {
33 | return _.findIndex(this.state.tabs, (tab) => {
34 | return tab.key === key;
35 | });
36 | }
37 |
38 | _saveStartPositions() {
39 | const positions = _.map(this.state.tabs, (tab) => {
40 | const el = this.refs[tab.key];
41 | const pos = el ? el.getBoundingClientRect() : {};
42 | return { key: tab.key, pos };
43 | });
44 | // Do not save in state
45 | this.startPositions = positions;
46 | }
47 |
48 | componentDidMount() {
49 | this._saveStartPositions();
50 | }
51 |
52 | componentWillReceiveProps(nextProps) {
53 | const newState = this._tabStateFromProps(nextProps);
54 | if (nextProps.selectedTab !== 'undefined') {
55 | newState.selectedTab = nextProps.selectedTab;
56 | }
57 | // reset closedTabs, respect props from application
58 | this.setState(newState);
59 | }
60 |
61 | componentDidUpdate() {
62 | this._saveStartPositions();
63 | }
64 |
65 | handleDrag(key, e) {
66 | const deltaX = (e.pageX || e.clientX);
67 | _.each(this.startPositions, (pos) => {
68 | const tempMoved = pos.moved || 0;
69 | const shoudBeSwap = key !== pos.key && pos.pos.left + tempMoved < deltaX && deltaX < pos.pos.right + tempMoved;
70 | if (shoudBeSwap) {
71 | const el = this.refs[pos.key];
72 | const idx1 = this._getIndexOfTabByKey(key);
73 | const idx2 = this._getIndexOfTabByKey(pos.key);
74 | const minus = idx1 > idx2 ? 1 : -1;
75 | const movePx = (minus * (pos.pos.right - pos.pos.left)) - tempMoved;
76 | el.style.transform = `translate(${movePx}px, 0px)`;
77 | this.startPositions[idx2].moved = movePx;
78 | }
79 | });
80 | }
81 |
82 | handleDragStop(key, e) {
83 | const deltaX = (e.pageX || e.clientX);
84 | let from;
85 | let to;
86 | for (let i = 0; i < this.startPositions.length; i++) {
87 | const pos = this.startPositions[i];
88 | const needSwap = key !== pos.key && pos.pos.left < deltaX && deltaX < pos.pos.right;
89 | if (needSwap) {
90 | from = key;
91 | to = pos.key;
92 | }
93 | const el = this.refs[pos.key];
94 | el.style.transform = 'translate(0px, 0px)';
95 | }
96 | if (from && to) {
97 | this.props.onTabPositionChange(from, to);
98 | }
99 | }
100 |
101 | handleTabClick(key) {
102 | this.props.onTabSelect(key);
103 | }
104 |
105 | handleCloseButtonClick(key, e) {
106 | e.preventDefault();
107 | e.stopPropagation();
108 | this.props.onTabClose(key);
109 | }
110 |
111 | handleAddButtonClick(e) {
112 | this.props.onTabAddButtonClick(e);
113 | }
114 |
115 | getCloseButton(tab) {
116 | if (tab.props.disableClose) {
117 | return '';
118 | }
119 | return (×);
120 | }
121 |
122 | render() {
123 | const tabs = _.map(this.state.tabs, (tab) => {
124 | const tabTitle = tab.props.title;
125 |
126 | return (
127 |
137 |
141 | {tabTitle}
142 |
144 |
145 |
146 | );
147 | });
148 |
149 | return
150 | {tabs}
151 |
152 | {this.props.tabAddButton}
153 |
154 |
;
155 | }
156 | }
157 |
158 | Tabs.defaultProps = {
159 | tabAddButton: ({'+'}),
160 | onTabSelect: () => {},
161 | onTabClose: () => {},
162 | onTabAddButtonClick: () => {},
163 | onTabPositionChange: () => {}
164 | };
165 |
166 | Tabs.propTypes = {
167 | tabs: PropTypes.arrayOf(PropTypes.element),
168 |
169 | selectedTab: PropTypes.string,
170 | tabAddButton: PropTypes.element,
171 | onTabSelect: PropTypes.func,
172 | onTabClose: PropTypes.func,
173 | onTabAddButtonClick: PropTypes.func,
174 | onTabPositionChange: PropTypes.func
175 |
176 | };
177 |
178 | export default Tabs;
179 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/client/vendors/jquery.context-menu/index.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 | /*!
3 | * jQuery contextMenu - Plugin for simple contextMenu handling
4 | *
5 | * Version: v2.0.0
6 | *
7 | * Authors: Björn Brala (SWIS.nl), Rodney Rehm, Addy Osmani (patches for FF)
8 | * Web: http://swisnl.github.io/jQuery-contextMenu/
9 | *
10 | * Copyright (c) 2011-2015 SWIS BV and contributors
11 | *
12 | * Licensed under
13 | * MIT License http://www.opensource.org/licenses/mit-license
14 | *
15 | * Date: 2015-11-16T21:31:33.969Z
16 | */
17 | @font-face {
18 | font-family: "context-menu-icons";
19 | font-style: normal;
20 | font-weight: normal;
21 |
22 | src: url("font/context-menu-icons.eot?4kdwx");
23 | src: url("font/context-menu-icons.eot?4kdwx#iefix") format("embedded-opentype"), url("font/context-menu-icons.woff2?4kdwx") format("woff2"), url("font/context-menu-icons.woff?4kdwx") format("woff"), url("font/context-menu-icons.ttf?4kdwx") format("truetype");
24 | }
25 |
26 | .context-menu-icon-add:before, .context-menu-icon-copy:before, .context-menu-icon-cut:before, .context-menu-icon-delete:before, .context-menu-icon-edit:before, .context-menu-icon-paste:before, .context-menu-icon-quit:before {
27 | position: absolute;
28 | top: 50%;
29 | left: 0;
30 | width: 28px;
31 | font-family: "context-menu-icons";
32 | font-size: 16px;
33 | font-style: normal;
34 | font-weight: normal;
35 | line-height: 1;
36 | color: #2980b9;
37 | text-align: center;
38 | -webkit-transform: translateY(-50%);
39 | -ms-transform: translateY(-50%);
40 | -o-transform: translateY(-50%);
41 | transform: translateY(-50%);
42 |
43 | -webkit-font-smoothing: antialiased;
44 | -moz-osx-font-smoothing: grayscale;
45 | }
46 |
47 | .context-menu-icon-add:before {
48 | content: "";
49 | }
50 |
51 | .context-menu-icon-add.context-menu-hover:before {
52 | color: #fff;
53 | }
54 |
55 | .context-menu-icon-copy:before {
56 | content: "";
57 | }
58 |
59 | .context-menu-icon-copy.context-menu-hover:before {
60 | color: #fff;
61 | }
62 |
63 | .context-menu-icon-cut:before {
64 | content: "";
65 | }
66 |
67 | .context-menu-icon-cut.context-menu-hover:before {
68 | color: #fff;
69 | }
70 |
71 | .context-menu-icon-delete:before {
72 | content: "";
73 | }
74 |
75 | .context-menu-icon-delete.context-menu-hover:before {
76 | color: #fff;
77 | }
78 |
79 | .context-menu-icon-edit:before {
80 | content: "";
81 | }
82 |
83 | .context-menu-icon-edit.context-menu-hover:before {
84 | color: #fff;
85 | }
86 |
87 | .context-menu-icon-paste:before {
88 | content: "";
89 | }
90 |
91 | .context-menu-icon-paste.context-menu-hover:before {
92 | color: #fff;
93 | }
94 |
95 | .context-menu-icon-quit:before {
96 | content: "";
97 | }
98 |
99 | .context-menu-icon-quit.context-menu-hover:before {
100 | color: #fff;
101 | }
102 |
103 | .context-menu-list {
104 | position: absolute;
105 | display: inline-block;
106 | min-width: 180px;
107 | max-width: 360px;
108 | padding: 4px 0;
109 | margin: 5px;
110 | font-family: inherit;
111 | font-size: inherit;
112 | white-space: pre;
113 | list-style-type: none;
114 | background: #fff;
115 | border: 1px solid #bebebe;
116 | border-radius: 3px;
117 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, .5);
118 | box-shadow: 0 2px 5px rgba(0, 0, 0, .5);
119 | }
120 |
121 | .context-menu-item {
122 | position: relative;
123 | padding: 3px 28px;
124 | color: #2f2f2f;
125 | -webkit-user-select: none;
126 | -moz-user-select: none;
127 | -ms-user-select: none;
128 | user-select: none;
129 | background-color: #fff;
130 | }
131 |
132 | .context-menu-separator {
133 | padding: 0;
134 | margin: 5px 0;
135 | border-bottom: 1px solid #e6e6e6;
136 | }
137 |
138 | .context-menu-item > label > input,
139 | .context-menu-item > label > textarea {
140 | -webkit-user-select: text;
141 | -moz-user-select: text;
142 | -ms-user-select: text;
143 | user-select: text;
144 | }
145 |
146 | .context-menu-item.context-menu-hover {
147 | color: #fff;
148 | cursor: pointer;
149 | background-color: #2980b9;
150 | }
151 |
152 | .context-menu-item.context-menu-disabled {
153 | color: #626262;
154 | background-color: #fff;
155 | }
156 |
157 | .context-menu-input.context-menu-hover,
158 | .context-menu-item.context-menu-disabled.context-menu-hover {
159 | cursor: default;
160 | background-color: #eee;
161 | }
162 |
163 | .context-menu-submenu:after {
164 | position: absolute;
165 | top: 50%;
166 | right: 8px;
167 | z-index: 1;
168 | width: 0;
169 | height: 0;
170 | content: '';
171 | border-color: transparent transparent transparent #2f2f2f;
172 | border-style: solid;
173 | border-width: 4px 0 4px 4px;
174 | -webkit-transform: translateY(-50%);
175 | -ms-transform: translateY(-50%);
176 | -o-transform: translateY(-50%);
177 | transform: translateY(-50%);
178 | }
179 |
180 | /**
181 | * Inputs
182 | */
183 | .context-menu-item.context-menu-input {
184 | padding: 5px 10px;
185 | }
186 |
187 | /* vertically align inside labels */
188 | .context-menu-input > label > * {
189 | vertical-align: top;
190 | }
191 |
192 | /* position checkboxes and radios as icons */
193 | .context-menu-input > label > input[type="checkbox"],
194 | .context-menu-input > label > input[type="radio"] {
195 | position: relative;
196 | top: 3px;
197 | }
198 |
199 | .context-menu-input > label,
200 | .context-menu-input > label > input[type="text"],
201 | .context-menu-input > label > textarea,
202 | .context-menu-input > label > select {
203 | display: block;
204 | width: 100%;
205 | -webkit-box-sizing: border-box;
206 | -moz-box-sizing: border-box;
207 | box-sizing: border-box;
208 | }
209 |
210 | .context-menu-input > label > textarea {
211 | height: 100px;
212 | }
213 |
214 | .context-menu-item > .context-menu-list {
215 | top: 5px;
216 | /* re-positioned by js */
217 | right: -5px;
218 | display: none;
219 | }
220 |
221 | .context-menu-item.context-menu-visible > .context-menu-list {
222 | display: block;
223 | }
224 |
225 | .context-menu-accesskey {
226 | text-decoration: underline;
227 | }
228 |
--------------------------------------------------------------------------------
/client/photon/template-app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Photon
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
56 |
57 |
58 |
59 |
60 |
61 | | Name |
62 | Kind |
63 | Date Modified |
64 | Author |
65 |
66 |
67 |
68 |
69 | | bars.scss |
70 | Document |
71 | Oct 13, 2015 |
72 | connors |
73 |
74 |
75 | | base.scss |
76 | Document |
77 | Oct 13, 2015 |
78 | connors |
79 |
80 |
81 | | button-groups.scss |
82 | Document |
83 | Oct 13, 2015 |
84 | connors |
85 |
86 |
87 | | buttons.scss |
88 | Document |
89 | Oct 13, 2015 |
90 | connors |
91 |
92 |
93 | | docs.scss |
94 | Document |
95 | Oct 13, 2015 |
96 | connors |
97 |
98 |
99 | | forms.scss |
100 | Document |
101 | Oct 13, 2015 |
102 | connors |
103 |
104 |
105 | | grid.scss |
106 | Document |
107 | Oct 13, 2015 |
108 | connors |
109 |
110 |
111 | | icons.scss |
112 | Document |
113 | Oct 13, 2015 |
114 | connors |
115 |
116 |
117 | | images.scss |
118 | Document |
119 | Oct 13, 2015 |
120 | connors |
121 |
122 |
123 | | lists.scss |
124 | Document |
125 | Oct 13, 2015 |
126 | connors |
127 |
128 |
129 | | mixins.scss |
130 | Document |
131 | Oct 13, 2015 |
132 | connors |
133 |
134 |
135 | | navs.scss |
136 | Document |
137 | Oct 13, 2015 |
138 | connors |
139 |
140 |
141 | | normalize.scss |
142 | Document |
143 | Oct 13, 2015 |
144 | connors |
145 |
146 |
147 | | photon.scss |
148 | Document |
149 | Oct 13, 2015 |
150 | connors |
151 |
152 |
153 | | tables.scss |
154 | Document |
155 | Oct 13, 2015 |
156 | connors |
157 |
158 |
159 | | tabs.scss |
160 | Document |
161 | Oct 13, 2015 |
162 | connors |
163 |
164 |
165 | | utilities.scss |
166 | Document |
167 | Oct 13, 2015 |
168 | connors |
169 |
170 |
171 | | variables.scss |
172 | Document |
173 | Oct 13, 2015 |
174 | connors |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/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 |
7 | require('./index.scss')
8 |
9 | class Terminal extends React.PureComponent {
10 | constructor() {
11 | super()
12 | this.onSelectBinded = this.onSelect.bind(this)
13 | }
14 |
15 | componentDidMount() {
16 | const {redis} = this.props
17 | redis.on('select', this.onSelectBinded)
18 | const terminal = this.terminal = $(this.refs.terminal).terminal((command, term) => {
19 | if (!command) {
20 | return
21 | }
22 | command = splitargs(command)
23 | const commandName = command[0] && command[0].toUpperCase()
24 | if (commandName === 'FLUSHALL' || commandName === 'FLUSHDB') {
25 | term.push(input => {
26 | if (input.match(/y|yes/i)) {
27 | this.execute(term, command)
28 | term.pop()
29 | } else if (input.match(/n|no/i)) {
30 | term.pop()
31 | }
32 | }, {
33 | prompt: '[[;#aac6e3;]Are you sure (y/n)? ]'
34 | })
35 | } else {
36 | this.execute(term, command)
37 | }
38 | }, {
39 | greetings: '',
40 | exit: false,
41 | completion(_, command, callback) {
42 | const commandName = command.split(' ')[0]
43 | const lower = commandName.toLowerCase()
44 | const isUppercase = commandName.toUpperCase() === commandName
45 | callback(
46 | commands.list
47 | .filter(item => item.indexOf(lower) === 0)
48 | .map(item => {
49 | const last = item.slice(commandName.length)
50 | return commandName + (isUppercase ? last.toUpperCase() : last)
51 | })
52 | )
53 | },
54 | name: this.props.connectionKey,
55 | height: '100%',
56 | width: '100%',
57 | outputLimit: 200,
58 | prompt: `[[;#fff;]redis> ]`,
59 | keydown(e) {
60 | if (!terminal.enabled()) {
61 | return true
62 | }
63 | if (e.ctrlKey || e.metaKey) {
64 | if (e.keyCode >= 48 && e.keyCode <= 57) {
65 | return true
66 | }
67 | if ([84, 87, 78, 82, 81].indexOf(e.keyCode) !== -1) {
68 | return true
69 | }
70 | }
71 | if (e.ctrlKey) {
72 | if (e.keyCode === 67) {
73 | if (terminal.level() > 1) {
74 | terminal.pop()
75 | if (terminal.paused()) {
76 | terminal.resume()
77 | }
78 | }
79 | return false
80 | }
81 | }
82 | }
83 | })
84 | }
85 |
86 | onSelect(db) {
87 | this.props.onDatabaseChange(db)
88 | }
89 |
90 | execute(term, args) {
91 | term.pause()
92 | const redis = this.props.redis
93 | if (args.length === 1 && args[0].toUpperCase() === 'MONITOR') {
94 | redis.monitor((_, monitor) => {
95 | term.echo('[[;#aac6e3;]Enter monitor mode. Press Ctrl+C to exit. ]')
96 | term.push(input => {
97 | }, {
98 | onExit() {
99 | monitor.disconnect()
100 | }
101 | })
102 | monitor.on('monitor', (time, args) => {
103 | if (term.level() > 1) {
104 | term.echo(formatMonitor(time, args), {raw: true})
105 | }
106 | })
107 | })
108 | } else if (args.length > 1 && ['SUBSCRIBE', 'PSUBSCRIBE'].indexOf(args[0].toUpperCase()) !== -1) {
109 | const newRedis = redis.duplicate()
110 | newRedis.call.apply(newRedis, args).then(res => {
111 | term.echo('[[;#aac6e3;]Enter subscription mode. Press Ctrl+C to exit. ]')
112 | term.push(input => {
113 | }, {
114 | onExit() {
115 | newRedis.disconnect()
116 | }
117 | })
118 | })
119 | newRedis.on('message', (channel, message) => {
120 | term.echo(formatMessage(channel, message), {raw: true})
121 | })
122 | newRedis.on('pmessage', (pattern, channel, message) => {
123 | term.echo(formatMessage(channel, message), {raw: true})
124 | })
125 | } else {
126 | redis.call.apply(redis, args).then(res => {
127 | term.echo(getHTML(res), {raw: true})
128 | term.resume()
129 | }).catch(err => {
130 | term.echo(getHTML(err), {raw: true})
131 | term.resume()
132 | })
133 | }
134 | }
135 |
136 | componentWillReceiveProps(nextProps) {
137 | if (this.props.style.display === 'none' && nextProps.style.display === 'block') {
138 | this.terminal.focus()
139 | }
140 | }
141 |
142 | componentWillUnmount() {
143 | this.props.redis.removeAllListeners('select', this.onSelectBinded)
144 | }
145 |
146 | render() {
147 | return ()
148 | }
149 | }
150 |
151 | export default Terminal
152 |
153 | function getHTML(response) {
154 | if (Array.isArray(response)) {
155 | return `
156 | ${response.map((item, index) => '- ' + index + '' + getHTML(item) + '
').join('')}
157 |
`
158 | }
159 | const type = typeof response
160 | if (type === 'number') {
161 | return `${response}
`
162 | }
163 | if (type === 'string') {
164 | return `${response.replace(/\r?\n/g, '
')}
`
165 | }
166 | if (response === null) {
167 | return `null
`
168 | }
169 | if (response instanceof Error) {
170 | return `${response.message}
`
171 | }
172 | if (type === 'object') {
173 | return `
174 | ${Object.keys(response).map(item => '- ' + item + '' + getHTML(response[item]) + '
').join('')}
175 | `
176 | }
177 |
178 | return `${JSON.stringify(response)}
`
179 | }
180 |
181 | function formatMonitor(time, args) {
182 | args = args || []
183 | const command = args[0] ? args.shift().toUpperCase() : ''
184 | if (command) {
185 | commands.getKeyIndexes(command.toLowerCase(), args).forEach(index => {
186 | args[index] = `${args[index]}`
187 | })
188 | }
189 | return `
190 | ${time}
191 |
192 | ${command}
193 | ${args.join(' ')}
194 |
195 |
`
196 | }
197 |
198 | function formatMessage(channel, message) {
199 | return `
200 | ${channel}
201 | ${message}
202 |
`
203 | }
204 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/SetContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from './BaseContent'
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 ReactDOM from 'react-dom'
10 |
11 | require('./BaseContent/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 | componentDidMount() {
115 | super.componentDidMount()
116 | $.contextMenu({
117 | context: ReactDOM.findDOMNode(this.refs.table),
118 | selector: '.' + this.randomClass,
119 | trigger: 'none',
120 | zIndex: 99999,
121 | callback: (key, opt) => {
122 | setTimeout(() => {
123 | if (key === 'delete') {
124 | this.deleteSelectedMember()
125 | }
126 | }, 0)
127 | ReactDOM.findDOMNode(this.refs.table).focus()
128 | },
129 | items: {
130 | delete: {name: 'Delete'}
131 | }
132 | })
133 | }
134 |
135 | showContextMenu(e, row) {
136 | this.handleSelect(null, row)
137 | $(ReactDOM.findDOMNode(this.refs.table)).contextMenu({
138 | x: e.pageX,
139 | y: e.pageY,
140 | zIndex: 99999
141 | })
142 | }
143 |
144 | render() {
145 | return (
153 |
159 |
169 | {
172 | const member = this.state.members[rowIndex]
173 | if (typeof member === 'undefined') {
174 | this.load(rowIndex)
175 | return 'Loading...'
176 | }
177 | return {member}
178 | }}
179 | header={
180 | {
182 | showModal({
183 | button: 'Insert Member',
184 | form: {
185 | type: 'object',
186 | properties: {
187 | 'Value:': {
188 | type: 'string'
189 | }
190 | }
191 | }
192 | }).then(res => {
193 | const data = res['Value:']
194 | return this.props.redis.sismember(this.state.keyName, data).then(exists => {
195 | if (exists) {
196 | const error = 'Member already exists'
197 | alert(error)
198 | throw new Error(error)
199 | }
200 | return data
201 | })
202 | }).then(data => {
203 | this.props.redis.sadd(this.state.keyName, data).then(() => {
204 | this.state.members.push(data)
205 | this.setState({
206 | members: this.state.members,
207 | length: this.state.length + 1
208 | }, () => {
209 | this.props.onKeyContentChange()
210 | this.handleSelect(null, this.state.members.length - 1)
211 | })
212 | })
213 | })
214 | }}
215 | />
216 | }
217 | />
218 |
219 |
220 |
224 | )
225 | }
226 | }
227 |
228 | export default SetContent
229 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/Editor/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import ReactDOM from 'react-dom'
5 | import Codemirror from '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 | this.state = {
29 | currentMode: '',
30 | wrapping: true,
31 | changed: false,
32 | modes: {
33 | raw: false,
34 | json: false,
35 | messagepack: false
36 | }
37 | }
38 | }
39 |
40 | updateLayout() {
41 | const $this = $(ReactDOM.findDOMNode(this))
42 | if ($this.width() < 372) {
43 | $(ReactDOM.findDOMNode(this.refs.wrapSelector)).hide()
44 | } else {
45 | $(ReactDOM.findDOMNode(this.refs.wrapSelector)).show()
46 | }
47 | this.refs.codemirror.getCodeMirror().refresh()
48 | }
49 |
50 | componentDidMount() {
51 | this.updateLayoutBinded = this.updateLayout.bind(this)
52 | $(window).on('resize', this.updateLayoutBinded)
53 | this.init(this.props.buffer)
54 | }
55 |
56 | componentWillUnmount() {
57 | $(window).off('resize', this.updateLayoutBinded)
58 | }
59 |
60 | componentWillReceiveProps(nextProps) {
61 | if (nextProps.buffer !== this.props.buffer) {
62 | this.init(nextProps.buffer)
63 | }
64 | }
65 |
66 | init(buffer) {
67 | if (!buffer) {
68 | this.setState({currentMode: '', changed: false})
69 | return
70 | }
71 | const content = buffer.toString()
72 | const modes = {}
73 | modes.raw = content
74 | modes.json = tryFormatJSON(content, true)
75 | modes.messagepack = modes.json ? false : tryFormatMessagepack(buffer, true)
76 | let currentMode = 'raw'
77 | if (modes.messagepack) {
78 | currentMode = 'messagepack'
79 | } else if (modes.json) {
80 | currentMode = 'json'
81 | }
82 | this.setState({modes, currentMode, changed: false}, () => {
83 | this.updateLayout()
84 | })
85 | }
86 |
87 | save() {
88 | let content = this.state.modes.raw
89 | if (this.state.currentMode === 'json') {
90 | content = tryFormatJSON(this.state.modes.json)
91 | if (!content) {
92 | alert('The json is invalid. Please check again.')
93 | return
94 | }
95 | } else if (this.state.currentMode === 'messagepack') {
96 | content = tryFormatMessagepack(this.state.modes.messagepack)
97 | if (!content) {
98 | alert('The json is invalid. Please check again.')
99 | return
100 | }
101 | content = msgpack.encode(JSON.parse(content))
102 | }
103 | this.props.onSave(content, err => {
104 | if (err) {
105 | alert(`Redis save failed: ${err.message}`)
106 | } else {
107 | this.init(typeof content === 'string' ? Buffer.from(content) : content)
108 | }
109 | })
110 | }
111 |
112 | updateContent(mode, content) {
113 | if (this.state.modes[mode] !== content) {
114 | this.state.modes[mode] = content
115 | this.setState({modes: this.state.modes, changed: true})
116 | }
117 | }
118 |
119 | updateMode(evt) {
120 | const newMode = evt.target.value
121 | this.setState({currentMode: newMode})
122 | }
123 |
124 | focus() {
125 | const codemirror = this.refs.codemirror
126 | if (codemirror) {
127 | const node = ReactDOM.findDOMNode(codemirror)
128 | if (node) {
129 | node.focus()
130 | }
131 | }
132 | }
133 |
134 | handleKeyDown(evt) {
135 | if (!evt.ctrlKey && evt.metaKey && evt.keyCode === 83) {
136 | this.save()
137 | evt.preventDefault()
138 | evt.stopPropagation()
139 | }
140 | }
141 |
142 | render() {
143 | let viewer
144 | if (this.state.currentMode === 'raw') {
145 | viewer = ()
158 | } else if (this.state.currentMode === 'json') {
159 | viewer = ()
180 | } else if (this.state.currentMode === 'messagepack') {
181 | viewer = ()
202 | } else {
203 | viewer =
204 | }
205 | return (
210 | { viewer }
211 |
214 |
222 |
231 |
236 |
237 |
)
238 | }
239 | }
240 |
241 | export default Editor
242 |
243 | function tryFormatJSON(jsonString, beautify) {
244 | try {
245 | const o = JSON.parse(jsonString)
246 | if (o && typeof o === 'object' && o !== null) {
247 | if (beautify) {
248 | return JSON.stringify(o, null, '\t')
249 | }
250 | return JSON.stringify(o)
251 | }
252 | } catch (e) { /**/ }
253 |
254 | return false
255 | }
256 |
257 | function tryFormatMessagepack(buffer, beautify) {
258 | try {
259 | let o
260 | if (typeof buffer === 'string') {
261 | o = JSON.parse(buffer)
262 | } else {
263 | o = msgpack.decode(buffer)
264 | }
265 | if (o && typeof o === 'object' && o !== null) {
266 | if (beautify) {
267 | return JSON.stringify(o, null, '\t')
268 | }
269 | return JSON.stringify(o)
270 | }
271 | } catch (e) { /**/ }
272 |
273 | return false
274 | }
275 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/ListContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from './BaseContent'
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 ReactDOM from 'react-dom'
11 |
12 | class ListContent extends BaseContent {
13 | save(value, callback) {
14 | if (typeof this.state.selectedIndex === 'number') {
15 | this.state.members[this.state.selectedIndex] = value.toString()
16 | this.setState({members: this.state.members})
17 | this.props.redis.lset(this.state.keyName, this.state.selectedIndex, 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.lpush(this.state.keyName, '')
28 | }
29 |
30 | load(index) {
31 | if (!super.load(index)) {
32 | return
33 | }
34 |
35 | const from = this.state.members.length
36 | const to = Math.min(from === 0 ? 200 : from + 1000, this.state.length - 1)
37 | if (to < from) {
38 | return
39 | }
40 |
41 | this.props.redis.lrange(this.state.keyName, from, to, (_, results) => {
42 | const diff = to - from + 1 - results.length
43 | this.setState({
44 | members: this.state.members.concat(results),
45 | length: this.state.length - diff
46 | }, () => {
47 | if (typeof this.state.selectedIndex !== 'number' && this.state.members.length) {
48 | this.handleSelect(null, 0)
49 | }
50 | this.loading = false
51 | if (this.state.members.length - 1 < this.maxRow && !diff) {
52 | this.load()
53 | }
54 | })
55 | })
56 | }
57 |
58 | handleOrderChange(desc) {
59 | this.setState({desc})
60 | }
61 |
62 | handleSelect(_, selectedIndex) {
63 | const content = this.state.members[this.state.desc ? this.state.length - 1 - selectedIndex : selectedIndex]
64 | if (typeof content !== 'undefined') {
65 | this.setState({selectedIndex, content})
66 | } else {
67 | this.setState({selectedIndex: null, content: null})
68 | }
69 | }
70 |
71 | deleteSelectedMember() {
72 | if (typeof this.state.selectedIndex !== 'number') {
73 | return
74 | }
75 | showModal({
76 | title: 'Delete selected item?',
77 | button: 'Delete',
78 | content: 'Are you sure you want to delete the selected item? This action cannot be undone.'
79 | }).then(() => {
80 | const members = this.state.members
81 | const deleted = members.splice(this.state.selectedIndex, 1)
82 | if (deleted.length) {
83 | this.props.redis.lremindex(this.state.keyName, this.state.selectedIndex)
84 | if (this.state.selectedIndex >= members.length - 1) {
85 | this.state.selectedIndex -= 1
86 | }
87 | this.setState({members, length: this.state.length - 1}, () => {
88 | this.props.onKeyContentChange()
89 | this.handleSelect(null, this.state.selectedIndex)
90 | })
91 | }
92 | })
93 | }
94 |
95 | handleKeyDown(e) {
96 | if (typeof this.state.selectedIndex === 'number') {
97 | if (e.keyCode === 8) {
98 | this.deleteSelectedMember()
99 | return false
100 | }
101 | if (e.keyCode === 38) {
102 | if (this.state.selectedIndex > 0) {
103 | this.handleSelect(null, this.state.selectedIndex - 1)
104 | }
105 | return false
106 | }
107 | if (e.keyCode === 40) {
108 | if (this.state.selectedIndex < this.state.members.length - 1) {
109 | this.handleSelect(null, this.state.selectedIndex + 1)
110 | }
111 | return false
112 | }
113 | }
114 | }
115 |
116 | componentDidMount() {
117 | super.componentDidMount()
118 | $.contextMenu({
119 | context: ReactDOM.findDOMNode(this.refs.table),
120 | selector: '.' + this.randomClass,
121 | trigger: 'none',
122 | zIndex: 99999,
123 | callback: (key, opt) => {
124 | setTimeout(() => {
125 | if (key === 'delete') {
126 | this.deleteSelectedMember()
127 | }
128 | }, 0)
129 | ReactDOM.findDOMNode(this.refs.table).focus()
130 | },
131 | items: {
132 | delete: {name: 'Delete'}
133 | }
134 | })
135 | }
136 |
137 | insert() {
138 | }
139 |
140 | showContextMenu(e, row) {
141 | this.handleSelect(null, row)
142 | $(ReactDOM.findDOMNode(this.refs.table)).contextMenu({
143 | x: e.pageX,
144 | y: e.pageY,
145 | zIndex: 99999
146 | })
147 | }
148 |
149 | render() {
150 | return (
157 |
163 |
175 | this.setState({
180 | desc,
181 | selectedIndex: typeof this.state.selectedIndex === 'number' ? this.state.length - 1 - this.state.selectedIndex : null
182 | })}
183 | desc={this.state.desc}
184 | />
185 | }
186 | width={this.props.indexBarWidth}
187 | isResizable
188 | cell={({rowIndex}) => {
189 | return { this.state.desc ? this.state.length - 1 - rowIndex : rowIndex }
190 | }}
191 | />
192 | {
196 | showModal({
197 | button: 'Insert Item',
198 | form: {
199 | type: 'object',
200 | properties: {
201 | 'Insert To:': {
202 | type: 'string',
203 | enum: ['head', 'tail']
204 | }
205 | }
206 | }
207 | }).then(res => {
208 | return res['Insert To:'] === 'head' ? 'lpush' : 'rpush'
209 | }).then(method => {
210 | const data = 'New Item'
211 | this.props.redis[method](this.state.keyName, data).then(() => {
212 | this.state.members[method === 'lpush' ? 'unshift' : 'push'](data)
213 | this.setState({
214 | members: this.state.members,
215 | length: this.state.length + 1
216 | }, () => {
217 | this.props.onKeyContentChange()
218 | this.handleSelect(null, method === 'lpush' ? 0 : this.state.members.length - 1)
219 | })
220 | })
221 | })
222 | }}
223 | />
224 | }
225 | width={this.props.contentBarWidth - this.props.indexBarWidth}
226 | cell={({rowIndex}) => {
227 | const data = this.state.members[this.state.desc ? this.state.length - 1 - rowIndex : rowIndex]
228 | if (typeof data === 'undefined') {
229 | this.load(rowIndex)
230 | return 'Loading...'
231 | }
232 | return { data }
233 | }}
234 | />
235 |
236 |
237 |
241 | )
242 | }
243 | }
244 |
245 | export default ListContent
246 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/ConnectionSelectorContainer/components/Config/index.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import store from 'Redux/store'
5 | import Immutable from 'immutable'
6 | import {remote} from 'electron'
7 | import fs from 'fs'
8 |
9 | require('./index.scss')
10 |
11 | class Config extends React.PureComponent {
12 | constructor() {
13 | super()
14 | this.state = {
15 | data: new Immutable.Map()
16 | }
17 | }
18 |
19 | getProp(property) {
20 | if (this.state.data.has(property)) {
21 | return this.state.data.get(property)
22 | }
23 | return this.props.favorite ? this.props.favorite.get(property) : ''
24 | }
25 |
26 | setProp(property, value) {
27 | this.setState({
28 | data: typeof property === 'string' ? this.state.data.set(property, value) : this.state.data.merge(property),
29 | changed: Boolean(this.props.favorite)
30 | })
31 | }
32 |
33 | componentWillReceiveProps(nextProps) {
34 | if (!this.props.connect && nextProps.connect) {
35 | this.connect()
36 | }
37 | if (this.props.favorite || nextProps.favorite) {
38 | const leaving = !this.props.favorite || !nextProps.favorite ||
39 | (this.props.favorite.get('key') !== nextProps.favorite.get('key'))
40 | if (leaving) {
41 | this.setState({changed: false, data: new Immutable.Map()})
42 | }
43 | }
44 | }
45 |
46 | connect() {
47 | const {favorite, connectToRedis} = this.props
48 | const data = this.state.data
49 | const config = favorite ? favorite.merge(data).toJS() : data.toJS()
50 | config.host = config.host || 'localhost'
51 | config.port = config.port || '6379'
52 | config.sshPort = config.sshPort || '22'
53 | connectToRedis(config)
54 | this.save()
55 | }
56 |
57 | handleChange(property, e) {
58 | let value = e.target.value
59 | if (property === 'ssh' || property === 'ssl') {
60 | value = e.target.checked
61 | }
62 | this.setProp(property, value)
63 | }
64 |
65 | duplicate() {
66 | if (this.props.favorite) {
67 | const data = Object.assign(this.props.favorite.toJS(), this.state.data.toJS())
68 | delete data.key
69 | this.props.onDuplicate(data)
70 | } else {
71 | const data = this.state.data.toJS()
72 | data.name = 'Quick Connect'
73 | this.props.onDuplicate(data)
74 | }
75 | }
76 |
77 | save() {
78 | if (this.props.favorite && this.state.changed) {
79 | this.props.onSave(this.state.data.toJS())
80 | this.setState({changed: false, data: new Immutable.Map()})
81 | }
82 | }
83 |
84 | renderCertInput(label, id) {
85 | return (
86 |
87 |
94 |
)
109 | }
110 |
111 | render() {
112 | return (
113 |
195 |
196 |
201 |
208 |
213 |
214 |
)
215 | }
216 | }
217 |
218 | export default Config
219 |
--------------------------------------------------------------------------------
/client/windows/MainWindow/components/InstanceContent/components/DatabaseContainer/components/Content/components/KeyContent/components/HashContent.jsx:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import React from 'react'
4 | import BaseContent from './BaseContent'
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} 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 | componentDidMount() {
103 | super.componentDidMount()
104 | $.contextMenu({
105 | context: ReactDOM.findDOMNode(this.refs.table),
106 | selector: '.' + this.randomClass,
107 | trigger: 'none',
108 | zIndex: 99999,
109 | callback: (key, opt) => {
110 | setTimeout(() => {
111 | if (key === 'delete') {
112 | this.deleteSelectedMember()
113 | } else if (key === 'copy') {
114 | clipboard.writeText(this.state.members[this.state.selectedIndex][0])
115 | } else if (key === 'rename') {
116 | this.setState({editableIndex: this.state.selectedIndex})
117 | }
118 | }, 0)
119 | ReactDOM.findDOMNode(this.refs.table).focus()
120 | },
121 | items: {
122 | copy: {name: 'Copy to Clipboard'},
123 | sep1: '---------',
124 | rename: {name: 'Rename Key'},
125 | delete: {name: 'Delete'}
126 | }
127 | })
128 | }
129 |
130 | showContextMenu(e, row) {
131 | this.handleSelect(null, row)
132 | $(ReactDOM.findDOMNode(this.refs.table)).contextMenu({
133 | x: e.pageX,
134 | y: e.pageY,
135 | zIndex: 99999
136 | })
137 | }
138 |
139 | render() {
140 | return (
147 |
154 |
{
161 | this.handleSelect(evt, index)
162 | this.setState({editableIndex: index})
163 | }}
164 | width={this.props.contentBarWidth}
165 | height={this.props.height}
166 | headerHeight={24}
167 | >
168 | {
172 | showModal({
173 | button: 'Insert Member',
174 | form: {
175 | type: 'object',
176 | properties: {
177 | 'Key:': {
178 | type: 'string'
179 | }
180 | }
181 | }
182 | }).then(res => {
183 | const data = res['Key:']
184 | const value = 'New Member'
185 | this.props.redis.hsetnx(this.state.keyName, data, value).then(inserted => {
186 | if (!inserted) {
187 | alert('The field already exists')
188 | return
189 | }
190 | this.state.members.push([data, Buffer.from(value)])
191 | this.setState({
192 | members: this.state.members,
193 | length: this.state.length + 1
194 | }, () => {
195 | this.props.onKeyContentChange()
196 | this.handleSelect(null, this.state.members.length - 1)
197 | })
198 | })
199 | })
200 | }}
201 | />
202 | }
203 | width={this.props.contentBarWidth}
204 | cell={({rowIndex}) => {
205 | const member = this.state.members[rowIndex]
206 | if (!member) {
207 | this.load(rowIndex)
208 | return 'Loading...'
209 | }
210 | return ( {
214 | const members = this.state.members
215 | const member = members[rowIndex]
216 | const keyName = this.state.keyName
217 | const source = member[0]
218 | if (source !== target && target) {
219 | this.props.redis.hexists(keyName, target).then(exists => {
220 | if (exists) {
221 | return showModal({
222 | title: 'Overwrite the field?',
223 | button: 'Overwrite',
224 | content: `Field "${target}" already exists. Are you sure you want to overwrite this field?`
225 | }).then(() => {
226 | let found
227 | for (let i = 0; i < members.length; i++) {
228 | if (members[i][0] === target) {
229 | found = i
230 | break
231 | }
232 | }
233 | if (typeof found === 'number') {
234 | members.splice(found, 1)
235 | this.setState({length: this.state.length - 1})
236 | }
237 | })
238 | }
239 | }).then(() => {
240 | member[0] = target
241 | this.props.redis.multi()
242 | .hdel(keyName, source)
243 | .hset(keyName, target, member[1]).exec()
244 | this.setState({members})
245 | }).catch(() => {})
246 | }
247 | this.setState({editableIndex: null})
248 | ReactDOM.findDOMNode(this).focus()
249 | }}
250 | html={member[0]}
251 | />)
252 | }}
253 | />
254 |
255 |
256 |
260 | )
261 | }
262 | }
263 |
264 | export default HashContent
265 |
--------------------------------------------------------------------------------