Browse Source

添加 EditPrompt 窗口。

oldj 8 years ago
parent
commit
161889066a

File diff suppressed because it is too large
+ 563 - 510
app/bundle.js


+ 1 - 1
src1/build/bundle.js

@@ -33993,7 +33993,7 @@ var Buttons = function (_React$Component) {
     });
 
     _Agent2.default.on('cancel_search', function () {
-      _this.calcelSearch();
+      _this.cancelSearch();
     });
 
     _Agent2.default.on('to_add_host', function () {

+ 1 - 1
src2/bundle.js

@@ -11584,7 +11584,7 @@ var Buttons = function (_React$Component) {
     });
 
     _Agent2.default.on('cancel_search', function () {
-      _this.calcelSearch();
+      _this.cancelSearch();
     });
 
     _Agent2.default.on('to_add_host', function () {

+ 7 - 7
ui/app.js

@@ -9,7 +9,7 @@ import React from 'react'
 import Panel from './panel/panel'
 import Content from './content/content'
 //import SudoPrompt from './frame/sudo'
-//import EditPrompt from './frame/edit'
+import EditPrompt from './frame/edit'
 //import PreferencesPrompt from './frame/preferences'
 import Agent from './Agent'
 import './app.less'
@@ -31,7 +31,7 @@ export default class App extends React.Component {
       this.setState({lang})
     })
 
-    Agent.on('toggle-hosts', (hosts, on) => {
+    Agent.on('toggle_hosts', (hosts, on) => {
       Agent.pact('toggleHosts', hosts.id, on)
         .then(() => {
           hosts.on = on
@@ -115,11 +115,11 @@ export default class App extends React.Component {
           setHostsContent={this.setHostsContent.bind(this)}
           lang={this.state.lang}
         />
-        {/*<div className="frames">*/}
-        {/*<SudoPrompt/>*/}
-        {/*<EditPrompt hosts={this.state.hosts}/>*/}
-        {/*<PreferencesPrompt/>*/}
-        {/*</div>*/}
+        <div className="frames">
+          {/*<SudoPrompt/>*/}
+          <EditPrompt lang={this.state.lang} list={this.state.list}/>
+          {/*<PreferencesPrompt/>*/}
+        </div>
       </div>
     )
   }

+ 305 - 0
ui/frame/edit.js

@@ -0,0 +1,305 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import MyFrame from './frame'
+import classnames from 'classnames'
+import Group from './group'
+import Agent from '../Agent'
+import './edit.less'
+
+export default class EditPrompt extends React.Component {
+  constructor (props) {
+    super(props)
+
+    this.state = {
+      show: false,
+      is_add: true,
+      where: 'local',
+      title: '',
+      url: '',
+      last_refresh: null,
+      refresh_interval: 0,
+      is_loading: false
+    }
+
+    this.current_host = null
+  }
+
+  tryToFocus () {
+    let el = this.refs.body && this.refs.body.querySelector('input[type=text]')
+    el && el.focus()
+  }
+
+  clear () {
+    this.setState({
+      where: 'local',
+      title: '',
+      url: '',
+      last_refresh: null,
+      refresh_interval: 0
+    })
+  }
+
+  componentDidMount () {
+    Agent.on('add_host', () => {
+      this.setState({
+        show: true,
+        add: true
+      })
+      setTimeout(() => {
+        this.tryToFocus()
+      }, 100)
+    })
+
+    Agent.on('edit_host', (host) => {
+      this.current_host = host
+      this.setState({
+        show: true,
+        add: false,
+        where: host.where || 'local',
+        title: host.title || '',
+        url: host.url || '',
+        last_refresh: host.last_refresh || null,
+        refresh_interval: host.refresh_interval || 0
+      })
+      setTimeout(() => {
+        this.tryToFocus()
+      }, 100)
+    })
+
+    Agent.on('loading_done', (old_host, data) => {
+      if (old_host === this.current_host) {
+        this.setState({
+          last_refresh: data.last_refresh,
+          is_loading: false
+        })
+        Agent.emit('host_refreshed', data, this.current_host)
+      }
+    })
+  }
+
+  onOK () {
+    this.setState({
+      title: (this.state.title || '').replace(/^\s+|\s+$/g, ''),
+      url: (this.state.url || '').replace(/^\s+|\s+$/g, '')
+    })
+
+    if (this.state.title === '') {
+      this.refs.title.focus()
+      return false
+    }
+    if (this.state.where === 'remote' && this.state.url === '') {
+      this.refs.url.focus()
+      return false
+    }
+
+    let data = Object.assign({}, this.current_host, this.state,
+      this.state.add ? {
+        content: `# ${this.state.title}`,
+        on: false
+      } : {})
+
+    if (!data.id) data.id = util.makeId()
+
+    delete data['add']
+    Agent.emit('host_' + (this.state.add ? 'add' : 'edit') + 'ed', data,
+      this.current_host)
+
+    this.setState({
+      show: false
+    })
+    this.clear()
+  }
+
+  onCancel () {
+    this.setState({
+      show: false
+    })
+    this.clear()
+  }
+
+  confirmDel () {
+    let {lang} = this.props
+    if (!confirm(lang.confirm_del)) return
+    Agent.emit('del_host', this.current_host)
+    this.setState({
+      show: false
+    })
+    this.clear()
+  }
+
+  getRefreshOptions () {
+    let {lang} = this.props
+    let k = [
+      [0, `${lang.never}`],
+      [1, `1 ${lang.hour}`],
+      [24, `24 ${lang.hours}`],
+      [168, `7 ${lang.days}`]
+    ]
+    if (Agent.IS_DEV) {
+      k.splice(1, 0, [0.002778, `10s (for DEV)`]) // dev test only
+    }
+    return k.map(([v, n], idx) => {
+      return (
+        <option value={v} key={idx}>{n}</option>
+      )
+    })
+  }
+
+  getEditOperations () {
+    if (this.state.add) return null
+
+    let {lang} = this.props
+
+    return (
+      <div>
+        <div className="ln">
+          <a href="#" className="del"
+             onClick={this.confirmDel.bind(this)}
+          >
+            <i className="iconfont icon-delete"/>
+            <span>{lang.del_host}</span>
+          </a>
+        </div>
+      </div>
+    )
+  }
+
+  refresh () {
+    if (this.state.is_loading) return
+
+    Agent.emit('check_host_refresh', this.current_host, true)
+    this.setState({
+      is_loading: true
+    }, () => {
+      setTimeout(() => {
+        this.setState({
+          is_loading: false
+        })
+      }, 1000)
+    })
+
+  }
+
+  renderGroup () {
+    if (this.state.where !== 'group') return null
+
+    return <Group list={this.props.list}/>
+  }
+
+  renderRemoteInputs () {
+    if (this.state.where !== 'remote') return null
+
+    let {lang} = this.props
+
+    return (
+      <div className="remote-ipts">
+        <div className="ln">
+          <div className="title">{lang.url}</div>
+          <div className="cnt">
+            <input
+              type="text"
+              ref="url"
+              value={this.state.url}
+              placeholder="http://"
+              onChange={(e) => this.setState({url: e.target.value})}
+              onKeyDown={(e) => (e.keyCode === 13 && this.onOK()) ||
+                                (e.keyCode === 27 && this.onCancel())}
+            />
+          </div>
+        </div>
+        <div className="ln">
+          <div className="title">{lang.auto_refresh}</div>
+          <div className="cnt">
+            <select
+              value={this.state.refresh_interval}
+              onChange={(e) => this.setState(
+                {refresh_interval: parseFloat(e.target.value) || 0})}
+            >
+              {this.getRefreshOptions()}
+            </select>
+
+            <i
+              className={classnames({
+                iconfont: 1,
+                'icon-refresh': 1,
+                'invisible': !this.current_host ||
+                             this.state.url !== this.current_host.url,
+                'loading': this.state.is_loading
+              })}
+              title={lang.refresh}
+              onClick={() => this.refresh()}
+            />
+
+            <span className="last-refresh">
+              {lang.last_refresh}
+              {this.state.last_refresh || 'N/A'}
+            </span>
+          </div>
+        </div>
+      </div>
+    )
+  }
+
+  body () {
+    let {lang} = this.props
+    return (
+      <div ref="body">
+        <div className="ln">
+          <input id="ipt-local" type="radio" name="where" value="local"
+                 checked={this.state.where === 'local'}
+                 onChange={(e) => this.setState({where: e.target.value})}
+          />
+          <label htmlFor="ipt-local">{lang.where_local}</label>
+          <input id="ipt-remote" type="radio" name="where" value="remote"
+                 checked={this.state.where === 'remote'}
+                 onChange={(e) => this.setState({where: e.target.value})}
+          />
+          <label htmlFor="ipt-remote">{lang.where_remote}</label>
+          <input id="ipt-remote" type="radio" name="where" value="group"
+                 checked={this.state.where === 'group'}
+                 onChange={(e) => this.setState({where: e.target.value})}
+          />
+          <label htmlFor="ipt-remote">{lang.where_group}</label>
+        </div>
+        <div className="ln">
+          <div className="title">{lang.host_title}</div>
+          <div className="cnt">
+            <input
+              type="text"
+              ref="title"
+              name="text"
+              value={this.state.title}
+              onChange={(e) => this.setState({title: e.target.value})}
+              onKeyDown={(e) => (e.keyCode === 13 && this.onOK() ||
+                                 e.keyCode === 27 && this.onCancel())}
+            />
+          </div>
+        </div>
+        {this.renderRemoteInputs()}
+        {this.renderGroup()}
+        {this.getEditOperations()}
+      </div>
+    )
+  }
+
+  render () {
+    let {lang} = this.props
+
+    return (
+      <MyFrame
+        show={this.state.show}
+        head={lang[this.state.add ? 'add_host' : 'edit_host']}
+        body={this.body()}
+        onOK={() => this.onOK()}
+        onCancel={() => this.onCancel()}
+        lang={this.props.lang}
+      />
+    )
+  }
+}

+ 55 - 0
ui/frame/edit.less

@@ -0,0 +1,55 @@
+@keyframes loading {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.frame {
+  label {
+    padding: 0 4em 0 0.5em;
+  }
+
+  .spiner {
+    transform-origin: right center;
+    animation: spin 1s;
+    animation-iteration-count: 1;
+    -webkit-animation-iteration-count: 1;
+  }
+
+  .ln {
+    i.icon-refresh {
+      display: inline-block;
+      color: #999;
+      margin: 0 0.6em;
+      cursor: pointer;
+
+      &:hover {
+        color: #333;
+      }
+
+      &.loading {
+        animation: loading 1s infinite linear;
+      }
+
+      &.invisible {
+        visibility: hidden;
+      }
+    }
+  }
+
+  .last-refresh {
+    //padding-left: 1em;
+    color: #999;
+  }
+
+  a.del {
+    color: red;
+
+    span {
+      padding-left: 0.5em;
+    }
+  }
+}

+ 74 - 0
ui/frame/frame.js

@@ -0,0 +1,74 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import Agent from '../Agent'
+import './frame.less'
+
+export default class MyFrame extends React.Component {
+  constructor (props) {
+    super(props)
+  }
+
+  componentDidMount () {
+    Agent.on('esc', () => {
+      this.onCancel()
+    })
+  }
+
+  onOK () {
+    this.props.onOK()
+  }
+
+  onCancel () {
+    this.props.onCancel()
+  }
+
+  renderFootButtons () {
+    let html = []
+    let {lang} = this.props
+
+    html.push(
+      <div
+        className="button btn-cancel"
+        key="btn-cancel"
+        onClick={this.onCancel.bind(this)}
+      >
+        {this.props.cancel_title || lang.cancel}
+      </div>
+    )
+
+    html.push(
+      <div
+        className="button btn-ok btn-default"
+        key="btn-ok"
+        onClick={this.onOK.bind(this)}
+      >
+        {this.props.ok_title || lang.ok}
+      </div>
+    )
+
+    return html
+  }
+
+  render () {
+    if (!this.props.show) {
+      return null
+    }
+
+    return (
+      <div className="frame" ref="frame">
+        <div className="overlay"/>
+        <div className="prompt">
+          <div className="head">{this.props.head}</div>
+          <div className="body">{this.props.body}</div>
+          <div className="foot">{this.renderFootButtons()}</div>
+        </div>
+      </div>
+    )
+  }
+}

+ 108 - 0
ui/frame/frame.less

@@ -0,0 +1,108 @@
+.frame {
+  @bg: #f5f5f5;
+  @btn_default: #05a;
+  @lh: 24px;
+
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+
+  .overlay {
+    position: absolute;
+    z-index: -1;
+    width: 100%;
+    height: 100%;
+    background: #000;
+    opacity: 0.5;
+  }
+
+  .prompt {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    min-width: 300px;
+    max-width: 600px;
+    background: #fff;
+    box-shadow: 0 0 4px 4px rgba(0, 0, 0, 0.1);
+
+    .head {
+      padding: 20px;
+      font-size: 16px;
+      background: @bg;
+      //border-bottom: solid 1px #ccc;
+    }
+
+    .body {
+      padding: 20px 20px;
+
+      .ln {
+        line-height: 30px;
+        padding: 2px 0;
+
+        .title {
+          float: left;
+          width: 100px;
+          line-height: @lh;
+        }
+        .cnt {
+          margin-left: 100px;
+          line-height: @lh;
+          input[type=text] {
+            width: 240px;
+            outline: none;
+            padding: 6px 10px;
+          }
+          textarea {
+            font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
+          }
+        }
+
+        .inform {
+          color: #999;
+          line-height: @lh;
+        }
+      }
+
+      input {
+        padding: 6px 4px;
+      }
+
+      input[type=password] {
+        letter-spacing: 8px;
+        width: 240px;
+        outline: none;
+        padding: 6px 10px;
+      }
+    }
+
+    .foot {
+      padding: 20px;
+      background: @bg;
+      text-align: right;
+
+      .button {
+        display: inline-block;
+        background: #ccc;
+        padding: 8px 20px;
+        margin-left: 1em;
+        cursor: pointer;
+
+        &:hover {
+          background: #ddd;
+        }
+
+        &.btn-default {
+          background: @btn_default;
+          color: #fff;
+
+          &:hover {
+            background: @btn_default * 1.4;
+          }
+        }
+      }
+    }
+  }
+}

+ 103 - 0
ui/frame/group.js

@@ -0,0 +1,103 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import classnames from 'classnames'
+import Sortable from 'sortablejs'
+import listToArray from 'wheel-js/src/common/list-to-array'
+import './group.less'
+
+export default class Group extends React.Component {
+  constructor (props) {
+    super(props)
+
+    this.state = {
+      list: []
+    }
+
+    this.current_host = null
+  }
+
+  makeItem (item) {
+    let attrs = {
+      'data-id': 'id:' + (item.id || '')
+    }
+    return (
+      <div className="hosts-item" {...attrs}>
+        <i className={classnames({
+          'iconfont': 1
+          , 'item-icon': 1
+          , 'icon-file': item.where !== 'group'
+          , 'icon-files': item.where === 'group'
+        })}
+        />
+        <span>{item.title}</span>
+      </div>
+    )
+  }
+
+  makeList () {
+    let items = this.state.list
+      .filter(item => item.where !== 'group')
+      .map(item => this.makeItem(item))
+
+    return (
+      <div id="hosts-group-valid">
+        <div ref="group_valid" className="hosts-group-list">
+          {items}
+        </div>
+      </div>
+    )
+  }
+
+  currentList () {
+    return (
+      <div id="hosts-group-current">
+        <div ref="group_current" className="hosts-group-list"></div>
+      </div>
+    )
+  }
+
+  getCurrentListFromDOM () {
+    let nodes = this.refs.group_current.getElementsByClassName('hosts-item')
+    nodes = listToArray(nodes)
+    console.log(nodes)
+  }
+
+  componentWillMount () {
+    console.log(1111)
+    console.log(this.props)
+    this.setState({
+      list: this.props.list
+    })
+  }
+
+  componentDidMount () {
+    Sortable.create(this.refs.group_valid, {
+      group: 'sorting'
+      , sort: false
+    })
+
+    Sortable.create(this.refs.group_current, {
+      group: 'sorting'
+      , sort: true
+      , onSort: evt => {
+        this.getCurrentListFromDOM()
+      }
+    })
+  }
+
+  render () {
+    return (
+      <div id="hosts-group">
+        {this.makeList()}
+        <div className="arrow"/>
+        {this.currentList()}
+      </div>
+    )
+  }
+}

+ 62 - 0
ui/frame/group.less

@@ -0,0 +1,62 @@
+#hosts-group {
+  @h: 160px;
+
+  position: relative;
+  margin-top: 20px;
+  height: @h + 2px;
+  overflow: auto;
+
+  &::after {
+    content: '';
+    clear: both;
+  }
+
+  .arrow {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    width: 20px;
+    height: 20px;
+    line-height: 20px;
+    text-align: center;
+    transform: translate(-50%, -50%);
+
+    &::after {
+      content: '→';
+    }
+  }
+
+  .hosts-group-list {
+    height: @h;
+    overflow: auto;
+    border: solid 1px #ccc;
+
+    .hosts-item {
+      cursor: move;
+      padding: 2px 6px;
+
+      &:hover {
+        background: #f5f5f5;
+      }
+
+      i {
+        font-size: 12px;
+        color: #999;
+      }
+
+      & > span {
+        padding-left: 6px;
+      }
+    }
+  }
+}
+
+#hosts-group-valid {
+  width: 45%;
+  float: left;
+}
+
+#hosts-group-current {
+  width: 45%;
+  float: right;
+}

+ 35 - 0
ui/frame/preferences.less

@@ -0,0 +1,35 @@
+.frame {
+  textarea {
+    width: 300px;
+    height: 80px;
+    padding: 2px 4px;
+    outline: none;
+    border: solid 1px #ccc;
+  }
+
+  .current-version {
+    float: right;
+    margin-top: -60px;
+    color: #999;
+
+    a {
+      color: #999;
+
+      &:hover {
+        color: #000;
+      }
+    }
+
+    &.update-found {
+      &:after {
+        content: '';
+        display: block;
+        float: right;
+        width: 6px;
+        height: 6px;
+        background: #f00;
+        border-radius: 3px;
+      }
+    }
+  }
+}

+ 7 - 0
ui/frame/sudo.less

@@ -0,0 +1,7 @@
+@import "frame";
+
+.frame {
+  .prompt {
+    width: 480px;
+  }
+}

+ 5 - 2
ui/package.json

@@ -12,7 +12,10 @@
     "classnames": "^2.2.5",
     "codemirror": "^5.25.0",
     "react": "^15.4.2",
-    "react-dom": "^15.4.2"
+    "react-dom": "^15.4.2",
+    "sortablejs": "^1.5.1",
+    "wheel-js": "0.0.2"
   },
-  "dependencies": {}
+  "dependencies": {
+  }
 }

+ 100 - 0
ui/panel/buttons.js

@@ -0,0 +1,100 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict';
+
+import React from 'react'
+import classnames from 'classnames'
+import Agent from '../Agent'
+import './buttons.less'
+
+export default class Buttons extends React.Component {
+  constructor (props) {
+    super(props)
+
+    this.state = {
+      top_toggle_on: true,
+      search_on: false
+    }
+
+    this.on_items = null
+  }
+
+  static btnAdd () {
+    Agent.emit('add_host')
+  }
+
+  btnToggle () {
+    if (this.state.top_toggle_on) {
+      Agent.emit('get_on_hosts', (items) => {
+        this.on_items = items
+      })
+    }
+
+    this.setState({
+      top_toggle_on: !this.state.top_toggle_on
+    }, () => {
+      Agent.emit('top_toggle', this.state.top_toggle_on, this.on_items)
+      if (this.state.top_toggle_on) {
+        this.on_items = null
+      }
+    })
+  }
+
+  btnSearch () {
+    this.setState({
+      search_on: !this.state.search_on
+    }, () => {
+      Agent.emit(this.state.search_on ? 'search_on' : 'search_off')
+    })
+  }
+
+  cancelSearch () {
+    this.setState({
+      search_on: false
+    }, () => {
+      Agent.emit('search_off')
+    })
+  }
+
+  componentDidMount () {
+    Agent.on('to_search', () => {
+      this.btnSearch()
+    })
+  }
+
+  render () {
+    return (
+      <div id="sh-buttons">
+        <div className="left">
+          <a
+            className="btn-add"
+            href="#"
+            onClick={() => Buttons.btnAdd()}
+          >+</a>
+        </div>
+
+        <div className="right">
+          <i
+            className={classnames({
+              iconfont: 1,
+              'icon-search': 1,
+              'on': this.state.search_on
+            })}
+            onClick={() => this.btnSearch()}
+          />
+          <i
+            className={classnames({
+              iconfont: 1,
+              'icon-switchon': this.state.top_toggle_on,
+              'icon-switchoff': !this.state.top_toggle_on
+            })}
+            onClick={() => this.btnToggle()}
+          />
+        </div>
+      </div>
+    )
+  }
+}

+ 1 - 1
ui/panel/list-item.js

@@ -31,7 +31,7 @@ export default class ListItem extends React.Component {
   toggle () {
     let on = !this.props.data.on
 
-    Agent.emit('toggle-hosts', this.props.data, on)
+    Agent.emit('toggle_hosts', this.props.data, on)
   }
 
   render () {

+ 4 - 4
ui/panel/panel.js

@@ -6,8 +6,8 @@
 'use strict'
 
 import React from 'react'
-//import Buttons from './buttons'
-//import SearchBar from './searchbar'
+import Buttons from './buttons'
+import SearchBar from './searchbar'
 import List from './list'
 import './panel.less'
 
@@ -16,8 +16,8 @@ export default class Panel extends React.Component {
     return (
       <div id="panel">
         <List {...this.props}/>
-        {/*<SearchBar/>*/}
-        {/*<Buttons/>*/}
+        <SearchBar/>
+        <Buttons/>
       </div>
     )
   }

+ 79 - 0
ui/panel/searchbar.js

@@ -0,0 +1,79 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import Agent from '../Agent'
+import './searchbar.less'
+
+export default class SearchBar extends React.Component {
+
+  constructor (props) {
+    super(props)
+
+    this.state = {
+      show: false,
+      keyword: ''
+    }
+
+    this._t = null
+
+    Agent.on('search_on', () => {
+      this.setState({
+        show: true
+      }, () => {
+        setTimeout(() => {
+          this.refs.keyword.focus()
+        }, 100)
+      })
+    })
+
+    Agent.on('search_off', () => {
+      this.clearSearch()
+    })
+  }
+
+  clearSearch () {
+    this.setState({
+      show: false,
+      keyword: ''
+    })
+    Agent.emit('search', '')
+  }
+
+  doSearch (kw) {
+    this.setState({
+      keyword: kw
+    })
+
+    clearTimeout(this._t)
+    this._t = setTimeout(() => {
+      Agent.emit('search', kw)
+    }, 300)
+  }
+
+  static onCancel () {
+    Agent.emit('cancel_search')
+  }
+
+  render () {
+    if (!this.state.show) {
+      return null
+    }
+    return (
+      <div id="sh-searchbar">
+        <input
+          ref="keyword"
+          type="text"
+          placeholder="keyword"
+          value={this.state.keyword}
+          onChange={(e) => this.doSearch(e.target.value)}
+          onKeyDown={(e) => (e.keyCode === 27 && SearchBar.onCancel())}
+        />
+      </div>
+    )
+  }
+}

Some files were not shown because too many files changed in this diff