├── 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 (
77 | { 78 | desc.map(({key, value}) => {value}) 82 | } 83 |
) 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 (
93 | Keys: {keys} 94 |
95 | DB: 96 | 119 |
120 |
) 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 |
97 | 104 | 123 |
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 |
{ 27 | patterns.map((pattern, index) => { 28 | return ( this.setState({index})} 32 | > 33 | {pattern.get('name')} 34 | ) 35 | }) 36 | }
37 |
38 | 45 | 54 |
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 | ![Medis for Windows](http://admin.waketu.com/medis.png) 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 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 8 | [![Join the chat at https://gitter.im/luin/medis](https://badges.gitter.im/Join%20Chat.svg)](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.
[![Download on the App Store](http://getmedis.com/download.svg)](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 |
17 |

Photon

18 |
19 | 20 | 21 |
22 |
23 | 56 | 57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 |
NameKindDate ModifiedAuthor
bars.scssDocumentOct 13, 2015connors
base.scssDocumentOct 13, 2015connors
button-groups.scssDocumentOct 13, 2015connors
buttons.scssDocumentOct 13, 2015connors
docs.scssDocumentOct 13, 2015connors
forms.scssDocumentOct 13, 2015connors
grid.scssDocumentOct 13, 2015connors
icons.scssDocumentOct 13, 2015connors
images.scssDocumentOct 13, 2015connors
lists.scssDocumentOct 13, 2015connors
mixins.scssDocumentOct 13, 2015connors
navs.scssDocumentOct 13, 2015connors
normalize.scssDocumentOct 13, 2015connors
photon.scssDocumentOct 13, 2015connors
tables.scssDocumentOct 13, 2015connors
tabs.scssDocumentOct 13, 2015connors
utilities.scssDocumentOct 13, 2015connors
variables.scssDocumentOct 13, 2015connors
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 |
      114 |
      115 | 116 | 117 |
      118 |
      119 | 120 | 121 |
      122 |
      123 | 124 | 125 |
      126 |
      127 | 128 | 129 |
      130 |
      131 | 132 | 133 |
      134 |
      135 | {this.renderCertInput('Private Key', 'tlskey')} 136 | {this.renderCertInput('Certificate', 'tlscert')} 137 | {this.renderCertInput('CA', 'tlsca')} 138 |
      139 |
      140 | 141 | 142 |
      143 |
      144 |
      145 | 146 | 147 |
      148 |
      149 | 150 | 151 |
      152 |
      153 | 154 | 162 |
      185 |
      -1 ? 'block' : 'none'}}> 186 | 187 | 188 |
      189 |
      190 | 191 | 192 |
      193 |
      194 |
      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 | --------------------------------------------------------------------------------