aboutsummaryrefslogtreecommitdiffstats
path: root/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'web/src')
-rw-r--r--web/src/js/components/Modal/OptionMaster.jsx257
-rw-r--r--web/src/js/ducks/options.js1
-rw-r--r--web/src/js/ducks/ui/option.js39
3 files changed, 296 insertions, 1 deletions
diff --git a/web/src/js/components/Modal/OptionMaster.jsx b/web/src/js/components/Modal/OptionMaster.jsx
new file mode 100644
index 00000000..5befc34a
--- /dev/null
+++ b/web/src/js/components/Modal/OptionMaster.jsx
@@ -0,0 +1,257 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { connect } from 'react-redux'
+import classnames from 'classnames'
+import { update as updateOptions } from '../../ducks/options'
+
+PureBooleanOption.PropTypes = {
+ value: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+
+function PureBooleanOption({ value, onChange, ...props}) {
+ return (
+ <input type="checkbox"
+ checked={value}
+ onChange={onChange}
+ onMouseOver={props.onMouseEnter}
+ onMouseLeave={props.onMouseLeave}
+ />
+ )
+}
+
+PureStringOption.PropTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+
+function PureStringOption( { value, onChange, ...props }) {
+ let onKeyDown = (e) => {e.stopPropagation()}
+ return (
+ <div className={classnames('input-group', {'has-error': props.error})}>
+ <input type="text"
+ value={value}
+ className='form-control'
+ onChange={onChange}
+ onKeyDown={onKeyDown}
+ onFocus={props.onFocus}
+ onBlur={props.onBlur}
+ onMouseOver={props.onMouseEnter}
+ onMouseLeave={props.onMouseLeave}
+ />
+ </div>
+ )
+}
+
+PureNumberOption.PropTypes = {
+ value: PropTypes.number.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+
+function PureNumberOption( {value, onChange, ...props }) {
+ let onKeyDown = (e) => {e.stopPropagation()}
+
+ return (
+ <input type="number"
+ className="form-control"
+ value={value}
+ onChange={onChange}
+ onKeyDown={onKeyDown}
+ onFocus={props.onFocus}
+ onBlur={props.onBlur}
+ onMouseOver={props.onMouseEnter}
+ onMouseLeave={props.onMouseLeave}
+ />
+ )
+}
+
+PureChoicesOption.PropTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+
+function PureChoicesOption( { value, onChange, name, choices, ...props}) {
+ return (
+ <select
+ name={name}
+ className="form-control"
+ onChange={onChange}
+ selected={value}
+ onFocus={props.onFocus}
+ onBlur={props.onBlur}
+ onMouseOver={props.onMouseEnter}
+ onMouseLeave={props.onMouseLeave}
+ >
+ { choices.map((choice, index) => (
+ <option key={index} value={choice}> {choice} </option>
+ ))}
+ </select>
+ )
+}
+
+class PureStringSequenceOption extends Component {
+ constructor(props, context) {
+ super(props, context)
+ this.state = { height: 1, focus: false, value: this.props.value}
+
+ this.onFocus = this.onFocus.bind(this)
+ this.onBlur = this.onBlur.bind(this)
+ this.onKeyDown = this.onKeyDown.bind(this)
+ this.onChange = this.onChange.bind(this)
+ }
+
+ onFocus() {
+ this.setState( {focus: true, height: 3 })
+ this.props.onFocus()
+ }
+
+ onBlur() {
+ this.setState( {focus: false, height: 1})
+ this.props.onBlur()
+ }
+
+ onKeyDown(e) {
+ e.stopPropagation()
+ }
+
+ onChange(e) {
+ const value = e.target.value.split("\n")
+ this.props.onChange(e)
+ this.setState({ value })
+ }
+
+ render() {
+ const {height, value} = this.state
+ const {error, onMouseEnter, onMouseLeave} = this.props
+ return (
+ <div className={classnames('input-group', {'has-error': error})}>
+ <textarea
+ rows={height}
+ value={value}
+ className="form-control"
+ onChange={this.onChange}
+ onKeyDown={this.onKeyDown}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ onMouseEnter={onMouseEnter}
+ onMouseLeave={onMouseLeave}
+ />
+ </div>
+ )
+ }
+}
+
+const OptionTypes = {
+ bool: PureBooleanOption,
+ str: PureStringOption,
+ int: PureNumberOption,
+ "optional str": PureStringOption,
+ "sequence of str": PureStringSequenceOption,
+}
+
+class OptionMaster extends Component {
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = {
+ option: this.props.option,
+ name: this.props.name,
+ mousefocus: false,
+ focus: false,
+ error: false,
+ }
+ if (props.option.choices) {
+ this.WrappedComponent = PureChoicesOption
+ } else {
+ this.WrappedComponent = OptionTypes[props.option.type]
+ }
+ this.onChange = this.onChange.bind(this)
+ this.onMouseEnter = this.onMouseEnter.bind(this)
+ this.onMouseLeave = this.onMouseLeave.bind(this)
+ this.onFocus = this.onFocus.bind(this)
+ this.onBlur = this.onBlur.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({ option: nextProps.option })
+ }
+
+ onChange(e) {
+ const { option, name } = this.state
+ const { updateOptions } = this.props
+ switch (option.type) {
+ case 'bool' :
+ updateOptions({[name]: !option.value})
+ break
+ case 'int':
+ updateOptions({[name]: parseInt(e.target.value)})
+ break
+ case 'sequence of str':
+ const value = e.target.value.split('\n')
+ updateOptions({[name]: value})
+ break
+ default:
+ updateOptions({[name]: e.target.value})
+ }
+ }
+
+ onMouseEnter() {
+ this.setState({ mousefocus: true })
+ }
+
+ onMouseLeave() {
+ this.setState({ mousefocus: false })
+ }
+
+ onFocus() {
+ this.setState({ focus: true })
+ }
+
+ onBlur() {
+ this.setState({ focus: false })
+ }
+
+ render() {
+ const { name, children, client_options } = this.props
+ const { option, focus, mousefocus } = this.state
+ const WrappedComponent = this.WrappedComponent
+ let error = (name in client_options) ? client_options[name].error : false,
+ value = (name in client_options) ? client_options[name].value : option.value
+ return (
+ <div className="row">
+ <div className="col-sm-8">
+ {name}
+ </div>
+ <div className="col-sm-4">
+ <WrappedComponent
+ children={children}
+ value={value}
+ onChange={this.onChange}
+ name={name}
+ choices={option.choices}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}
+ error={error}
+ />
+ {(focus || mousefocus) && (
+ <div className="tooltip tooltip-bottom" role="tooltip" style={{opacity: 1}}>
+ <div className="tooltip-inner">
+ {option.help}
+ </div>
+ </div>)}
+ </div>
+ </div>
+ )
+ }
+}
+
+export default connect(
+ state => ({
+ client_options: state.ui.option
+ }),
+ {
+ updateOptions
+ }
+)(OptionMaster)
diff --git a/web/src/js/ducks/options.js b/web/src/js/ducks/options.js
index 286a1ae3..3277fb9e 100644
--- a/web/src/js/ducks/options.js
+++ b/web/src/js/ducks/options.js
@@ -25,7 +25,6 @@ export default function reducer(state = defaultState, action) {
}
}
-
let sendUpdate = (option, value, dispatch) => {
fetchApi.put('/options', { [option]: value }).then(response => {
if (response.status === 200) {
diff --git a/web/src/js/ducks/ui/option.js b/web/src/js/ducks/ui/option.js
new file mode 100644
index 00000000..6aba4998
--- /dev/null
+++ b/web/src/js/ducks/ui/option.js
@@ -0,0 +1,39 @@
+export const OPTION_UPDATE_START = 'UI_OPTION_UPDATE_START'
+export const OPTION_UPDATE_SUCCESS = 'UI_OPTION_UPDATE_SUCCESS'
+export const OPTION_UPDATE_ERROR = 'UI_OPTION_UPDATE_ERROR'
+
+const defaultState = {
+ /* optionName -> {isUpdating, value (client-side), error} */
+}
+
+export default function reducer(state = defaultState, action) {
+ switch (action.type) {
+ case OPTION_UPDATE_START:
+ return {
+ ...state,
+ [action.option]: {
+ isUpdate: true,
+ value: action.value,
+ error: false,
+ }
+ }
+
+ case OPTION_UPDATE_SUCCESS:
+ let s = {...state}
+ delete s[action.option]
+ return s
+
+ case OPTION_UPDATE_ERROR:
+ return {
+ ...state,
+ [action.option]: {
+ ...state[action.option],
+ isUpdating: false,
+ error: action.error
+ }
+ }
+
+ default:
+ return state
+ }
+}