aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/src/js/__tests__/components/Modal/OptionSpec.js99
-rw-r--r--web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap235
-rw-r--r--web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap64
-rw-r--r--web/src/js/__tests__/ducks/optionsSpec.js32
-rw-r--r--web/src/js/__tests__/ducks/tutils.js6
-rw-r--r--web/src/js/__tests__/ducks/ui/keyboardSpec.js7
-rw-r--r--web/src/js/__tests__/ducks/ui/optionEditorSpec.js32
-rw-r--r--web/src/js/components/Modal/Option.jsx141
-rw-r--r--web/src/js/components/Modal/OptionMaster.jsx119
-rw-r--r--web/src/js/components/Modal/OptionModal.jsx57
-rw-r--r--web/src/js/ducks/connection.js2
-rw-r--r--web/src/js/ducks/options.js34
-rw-r--r--web/src/js/ducks/settings.js1
-rw-r--r--web/src/js/ducks/ui/index.js4
-rw-r--r--web/src/js/ducks/ui/keyboard.js7
-rw-r--r--web/src/js/ducks/ui/optionsEditor.js73
16 files changed, 678 insertions, 235 deletions
diff --git a/web/src/js/__tests__/components/Modal/OptionSpec.js b/web/src/js/__tests__/components/Modal/OptionSpec.js
new file mode 100644
index 00000000..a275aee6
--- /dev/null
+++ b/web/src/js/__tests__/components/Modal/OptionSpec.js
@@ -0,0 +1,99 @@
+import React from 'react'
+import renderer from 'react-test-renderer'
+import { Options, ChoicesOption } from '../../../components/Modal/Option'
+
+describe('BooleanOption Component', () => {
+ let BooleanOption = Options['bool'],
+ onChangeFn = jest.fn(),
+ booleanOption = renderer.create(
+ <BooleanOption value={true} onChange={onChangeFn}/>
+ ),
+ tree = booleanOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let input = tree.children[0].children[0],
+ mockEvent = { target: { checked: true }}
+ input.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(mockEvent.target.checked)
+ })
+})
+
+describe('StringOption Component', () => {
+ let StringOption = Options['str'],
+ onChangeFn = jest.fn(),
+ stringOption = renderer.create(
+ <StringOption value="foo" onChange={onChangeFn}/>
+ ),
+ tree = stringOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let mockEvent = { target: { value: 'bar' }}
+ tree.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(mockEvent.target.value)
+ })
+
+})
+
+describe('NumberOption Component', () => {
+ let NumberOption = Options['int'],
+ onChangeFn = jest.fn(),
+ numberOption = renderer.create(
+ <NumberOption value={1} onChange={onChangeFn}/>
+ ),
+ tree = numberOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let mockEvent = {target: { value: '2'}}
+ tree.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(2)
+ })
+})
+
+describe('ChoiceOption Component', () => {
+ let onChangeFn = jest.fn(),
+ choiceOption = renderer.create(
+ <ChoicesOption value='a' choices={['a', 'b', 'c']} onChange={onChangeFn}/>
+ ),
+ tree = choiceOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let mockEvent = { target: {value: 'b'} }
+ tree.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(mockEvent.target.value)
+ })
+})
+
+describe('StringOption Component', () => {
+ let onChangeFn = jest.fn(),
+ StringSequenceOption = Options['sequence of str'],
+ stringSequenceOption = renderer.create(
+ <StringSequenceOption value={['a', 'b']} onChange={onChangeFn}/>
+ ),
+ tree = stringSequenceOption.toJSON()
+
+ it('should render correctly', () => {
+ expect(tree).toMatchSnapshot()
+ })
+
+ it('should handle onChange', () => {
+ let mockEvent = { target: {value: 'a\nb\nc\n'}}
+ tree.props.onChange(mockEvent)
+ expect(onChangeFn).toBeCalledWith(['a', 'b', 'c', ''])
+ })
+})
diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap
index af587ae4..bfd855bd 100644
--- a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap
+++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap
@@ -47,94 +47,173 @@ exports[`Modal Component should render correctly 2`] = `
className="modal-body"
>
<div
- className="menu-entry"
+ className="form-horizontal"
>
- <label>
- booleanOption
- <input
- checked={false}
- onChange={[Function]}
- title="foo"
- type="checkbox"
- />
- </label>
- </div>
- <div
- className="menu-entry"
- >
- <label
- htmlFor=""
+ <div
+ className="form-group"
>
- choiceOption
- <select
- name="choiceOption"
- onChange={[Function]}
- selected="b"
- title="foo"
+ <div
+ className="col-xs-6"
>
- <option
- value="a"
+ <label
+ htmlFor="booleanOption"
>
-
- a
-
- </option>
- <option
- value="b"
+ booleanOption
+ </label>
+ <div
+ className="help-block small"
>
-
- b
-
- </option>
- <option
- value="c"
+ foo
+ </div>
+ </div>
+ <div
+ className="col-xs-6"
+ >
+ <div
+ className=""
>
-
- c
-
- </option>
- </select>
- </label>
- </div>
- <div
- className="menu-entry"
- >
- <label>
- intOption
- <input
- onChange={[Function]}
- onKeyDown={[Function]}
- title="foo"
- type="number"
- value={1}
- />
- </label>
- </div>
- <div
- className="menu-entry"
- >
- <label>
- strOption
- <input
- onChange={[Function]}
- onKeyDown={[Function]}
- title="foo"
- type="text"
- value="str content"
- />
- </label>
+ <div
+ className="checkbox"
+ >
+ <label>
+ <input
+ checked={false}
+ name="booleanOption"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ type="checkbox"
+ />
+ Enable
+ </label>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <div
+ className="col-xs-6"
+ >
+ <label
+ htmlFor="choiceOption"
+ >
+ choiceOption
+ </label>
+ <div
+ className="help-block small"
+ >
+ foo
+ </div>
+ </div>
+ <div
+ className="col-xs-6"
+ >
+ <div
+ className=""
+ >
+ <select
+ className="form-control"
+ name="choiceOption"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ selected="b"
+ >
+ <option
+ value="a"
+ >
+ a
+ </option>
+ <option
+ value="b"
+ >
+ b
+ </option>
+ <option
+ value="c"
+ >
+ c
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <div
+ className="col-xs-6"
+ >
+ <label
+ htmlFor="intOption"
+ >
+ intOption
+ </label>
+ <div
+ className="help-block small"
+ >
+ foo
+ </div>
+ </div>
+ <div
+ className="col-xs-6"
+ >
+ <div
+ className=""
+ >
+ <input
+ className="form-control"
+ name="intOption"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ type="number"
+ value={1}
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ className="form-group"
+ >
+ <div
+ className="col-xs-6"
+ >
+ <label
+ htmlFor="strOption"
+ >
+ strOption
+ </label>
+ <div
+ className="help-block small"
+ >
+ foo
+ </div>
+ </div>
+ <div
+ className="col-xs-6"
+ >
+ <div
+ className="has-error"
+ >
+ <input
+ className="form-control"
+ name="strOption"
+ onChange={[Function]}
+ onKeyDown={[Function]}
+ type="text"
+ value="str content"
+ />
+ </div>
+ <div
+ className="small text-danger"
+ />
+ </div>
+ </div>
</div>
</div>
<div
className="modal-footer"
- >
- <button
- className="btn btn-primary"
- type="button"
- >
- Save Changes
- </button>
- </div>
+ />
</div>
</div>
</div>
diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap
new file mode 100644
index 00000000..514e0eb5
--- /dev/null
+++ b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap
@@ -0,0 +1,64 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BooleanOption Component should render correctly 1`] = `
+<div
+ className="checkbox"
+>
+ <label>
+ <input
+ checked={true}
+ onChange={[Function]}
+ type="checkbox"
+ />
+ Enable
+ </label>
+</div>
+`;
+
+exports[`ChoiceOption Component should render correctly 1`] = `
+<select
+ onChange={[Function]}
+ selected="a"
+>
+ <option
+ value="a"
+ >
+ a
+ </option>
+ <option
+ value="b"
+ >
+ b
+ </option>
+ <option
+ value="c"
+ >
+ c
+ </option>
+</select>
+`;
+
+exports[`NumberOption Component should render correctly 1`] = `
+<input
+ onChange={[Function]}
+ type="number"
+ value={1}
+/>
+`;
+
+exports[`StringOption Component should render correctly 1`] = `
+<input
+ onChange={[Function]}
+ type="text"
+ value="foo"
+/>
+`;
+
+exports[`StringOption Component should render correctly 2`] = `
+<textarea
+ onChange={[Function]}
+ rows={2}
+ value="a
+b"
+/>
+`;
diff --git a/web/src/js/__tests__/ducks/optionsSpec.js b/web/src/js/__tests__/ducks/optionsSpec.js
index 62019715..0925fcc1 100644
--- a/web/src/js/__tests__/ducks/optionsSpec.js
+++ b/web/src/js/__tests__/ducks/optionsSpec.js
@@ -1,6 +1,9 @@
-jest.mock('../../utils')
-
import reduceOptions, * as OptionsActions from '../../ducks/options'
+import configureStore from 'redux-mock-store'
+import thunk from 'redux-thunk'
+import * as OptionsEditorActions from '../../ducks/ui/optionsEditor'
+
+const mockStore = configureStore([ thunk ])
describe('option reducer', () => {
it('should return initial state', () => {
@@ -18,8 +21,31 @@ describe('option reducer', () => {
})
})
+let store = mockStore()
+
describe('option actions', () => {
+
it('should be possible to update option', () => {
- expect(reduceOptions(undefined, OptionsActions.update())).toEqual({})
+ let mockResponse = { status: 200 },
+ promise = Promise.resolve(mockResponse)
+ global.fetch = r => { return promise }
+ store.dispatch(OptionsActions.update('foo', 'bar'))
+ expect(store.getActions()).toEqual([
+ { type: OptionsEditorActions.OPTION_UPDATE_START, option: 'foo', value: 'bar'}
+ ])
+ store.clearActions()
+ })
+})
+
+describe('sendUpdate', () => {
+
+ it('should handle error', () => {
+ let mockResponse = { status: 400, text: p => Promise.resolve('error') },
+ promise = Promise.resolve(mockResponse)
+ global.fetch = r => { return promise }
+ OptionsActions.pureSendUpdate('bar', 'error')
+ expect(store.getActions()).toEqual([
+ { type: OptionsEditorActions.OPTION_UPDATE_SUCCESS, option: 'foo'}
+ ])
})
})
diff --git a/web/src/js/__tests__/ducks/tutils.js b/web/src/js/__tests__/ducks/tutils.js
index a3e9c168..22240448 100644
--- a/web/src/js/__tests__/ducks/tutils.js
+++ b/web/src/js/__tests__/ducks/tutils.js
@@ -35,6 +35,12 @@ export function TStore(){
},
modal: {
activeModal: undefined
+ },
+ optionsEditor: {
+ booleanOption: { isUpdating: true, error: false },
+ strOption: { error: true },
+ intOption: {},
+ choiceOption: {},
}
},
settings: {
diff --git a/web/src/js/__tests__/ducks/ui/keyboardSpec.js b/web/src/js/__tests__/ducks/ui/keyboardSpec.js
index 500733cb..cf17943f 100644
--- a/web/src/js/__tests__/ducks/ui/keyboardSpec.js
+++ b/web/src/js/__tests__/ducks/ui/keyboardSpec.js
@@ -6,6 +6,7 @@ import reduceFlows from '../../../ducks/flows'
import reduceUI from '../../../ducks/ui/index'
import * as flowsActions from '../../../ducks/flows'
import * as UIActions from '../../../ducks/ui/flow'
+import * as modalActions from '../../../ducks/ui/modal'
import configureStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import { fetchApi } from '../../../utils'
@@ -154,4 +155,10 @@ describe('onKeyDown', () => {
expect(fetchApi).not.toBeCalled()
})
+ it('should close modal', () => {
+ store.getState().ui.modal.activeModal = true
+ store.dispatch(createKeyEvent(Key.ESC))
+ expect(store.getActions()).toEqual([ {type: modalActions.HIDE_MODAL} ])
+ })
+
})
diff --git a/web/src/js/__tests__/ducks/ui/optionEditorSpec.js b/web/src/js/__tests__/ducks/ui/optionEditorSpec.js
new file mode 100644
index 00000000..df9161a4
--- /dev/null
+++ b/web/src/js/__tests__/ducks/ui/optionEditorSpec.js
@@ -0,0 +1,32 @@
+import reduceOptionsEditor, * as optionsEditorActions from '../../../ducks/ui/optionsEditor'
+import { HIDE_MODAL } from '../../../ducks/ui/modal'
+
+describe('optionsEditor reducer', () => {
+
+ it('should return initial state', () => {
+ expect(reduceOptionsEditor(undefined, {})).toEqual({})
+ })
+
+ let state = undefined
+ it('should handle option update start', () => {
+ state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', 'bar'))
+ expect(state).toEqual({ foo: {error: false, isUpdating: true, value: 'bar'}})
+ })
+
+ it('should handle option update success', () => {
+ expect(reduceOptionsEditor(state, optionsEditorActions.updateSuccess('foo'))).toEqual({foo: undefined})
+ })
+
+ it('should handle option update error', () => {
+ state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg'))
+ expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: 'bar'}})
+ // boolean type
+ state = reduceOptionsEditor(undefined, optionsEditorActions.startUpdate('foo', true))
+ state = reduceOptionsEditor(state, optionsEditorActions.updateError('foo', 'errorMsg'))
+ expect(state).toEqual({ foo: {error: 'errorMsg', isUpdating: false, value: false}})
+ })
+
+ it('should handle hide modal', () => {
+ expect(reduceOptionsEditor(undefined, {type: HIDE_MODAL})).toEqual({})
+ })
+})
diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx
new file mode 100644
index 00000000..58b863d1
--- /dev/null
+++ b/web/src/js/components/Modal/Option.jsx
@@ -0,0 +1,141 @@
+import React, { Component } from "react"
+import PropTypes from "prop-types"
+import { connect } from "react-redux"
+import { update as updateOptions } from "../../ducks/options"
+import { Key } from "../../utils"
+import classnames from 'classnames'
+
+const stopPropagation = e => {
+ if (e.keyCode !== Key.ESC) {
+ e.stopPropagation()
+ }
+}
+
+BooleanOption.PropTypes = {
+ value: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+function BooleanOption({ value, onChange, ...props }) {
+ return (
+ <div className="checkbox">
+ <label>
+ <input type="checkbox"
+ checked={value}
+ onChange={e => onChange(e.target.checked)}
+ {...props}
+ />
+ Enable
+ </label>
+ </div>
+ )
+}
+
+StringOption.PropTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+function StringOption({ value, onChange, ...props }) {
+ return (
+ <input type="text"
+ value={value || ""}
+ onChange={e => onChange(e.target.value)}
+ {...props}
+ />
+ )
+}
+function Optional(Component) {
+ return function ({ onChange, ...props }) {
+ return <Component
+ onChange={x => onChange(x ? x : null)}
+ {...props}
+ />
+ }
+}
+
+NumberOption.PropTypes = {
+ value: PropTypes.number.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+function NumberOption({ value, onChange, ...props }) {
+ return (
+ <input type="number"
+ value={value}
+ onChange={(e) => onChange(parseInt(e.target.value))}
+ {...props}
+ />
+ )
+}
+
+ChoicesOption.PropTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+export function ChoicesOption({ value, onChange, choices, ...props }) {
+ return (
+ <select
+ onChange={(e) => onChange(e.target.value)}
+ selected={value}
+ {...props}
+ >
+ { choices.map(
+ choice => (
+ <option key={choice} value={choice}>{choice}</option>
+ )
+ )}
+ </select>
+ )
+}
+
+StringSequenceOption.PropTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+}
+function StringSequenceOption({ value, onChange, ...props }) {
+ const height = Math.max(value.length, 1)
+ return <textarea
+ rows={height}
+ value={value.join('\n')}
+ onChange={e => onChange(e.target.value.split("\n"))}
+ {...props}
+ />
+}
+
+export const Options = {
+ "bool": BooleanOption,
+ "str": StringOption,
+ "int": NumberOption,
+ "optional str": Optional(StringOption),
+ "sequence of str": StringSequenceOption,
+}
+
+function PureOption({ choices, type, value, onChange, name, error }) {
+ let Opt, props = {}
+ if (choices) {
+ Opt = ChoicesOption;
+ props.choices = choices
+ } else {
+ Opt = Options[type]
+ }
+ if (Opt !== BooleanOption) {
+ props.className = "form-control"
+ }
+
+ return <div className={classnames({'has-error':error})}>
+ <Opt
+ name={name}
+ value={value}
+ onChange={onChange}
+ onKeyDown={stopPropagation}
+ {...props}
+ />
+ </div>
+}
+export default connect(
+ (state, { name }) => ({
+ ...state.options[name],
+ ...state.ui.optionsEditor[name]
+ }),
+ (dispatch, { name }) => ({
+ onChange: value => dispatch(updateOptions(name, value))
+ })
+)(PureOption)
diff --git a/web/src/js/components/Modal/OptionMaster.jsx b/web/src/js/components/Modal/OptionMaster.jsx
deleted file mode 100644
index c25dda72..00000000
--- a/web/src/js/components/Modal/OptionMaster.jsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import PropTypes from 'prop-types'
-
-PureBooleanOption.PropTypes = {
- value: PropTypes.bool.isRequired,
- onChange: PropTypes.func.isRequired,
-}
-
-function PureBooleanOption({ value, onChange, name, help}) {
- return (
- <label>
- { name }
- <input type="checkbox"
- checked={value}
- onChange={onChange}
- title={help}
- />
- </label>
- )
-}
-
-PureStringOption.PropTypes = {
- value: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
-}
-
-function PureStringOption( { value, onChange, name, help }) {
- let onKeyDown = (e) => {e.stopPropagation()}
- return (
- <label>
- { name }
- <input type="text"
- value={value}
- onChange={onChange}
- title={help}
- onKeyDown={onKeyDown}
- />
- </label>
- )
-}
-
-PureNumberOption.PropTypes = {
- value: PropTypes.number.isRequired,
- onChange: PropTypes.func.isRequired,
-}
-
-function PureNumberOption( {value, onChange, name, help }) {
- let onKeyDown = (e) => {e.stopPropagation()}
- return (
- <label>
- { name }
- <input type="number"
- value={value}
- onChange={onChange}
- title={help}
- onKeyDown={onKeyDown}
- />
- </label>
- )
-}
-
-PureChoicesOption.PropTypes = {
- value: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
-}
-
-function PureChoicesOption( { value, onChange, name, help, choices }) {
- return (
- <label htmlFor="">
- { name }
- <select name={name} onChange={onChange} title={help} selected={value}>
- { choices.map((choice, index) => (
- <option key={index} value={choice}> {choice} </option>
- ))}
- </select>
- </label>
- )
-}
-
-const OptionTypes = {
- bool: PureBooleanOption,
- str: PureStringOption,
- int: PureNumberOption,
- "optional str": PureStringOption,
- "sequence of str": PureStringOption,
-}
-
-export default function OptionMaster({option, name, updateOptions, ...props}) {
- let WrappedComponent = null
- if (option.choices) {
- WrappedComponent = PureChoicesOption
- } else {
- WrappedComponent = OptionTypes[option.type]
- }
-
- let onChange = (e) => {
- switch (option.type) {
- case 'bool' :
- updateOptions({[name]: !option.value})
- break
- case 'int':
- updateOptions({[name]: parseInt(e.target.value)})
- break
- default:
- updateOptions({[name]: e.target.value})
- }
- }
- return (
- <div className="menu-entry">
- <WrappedComponent
- children={props.children}
- value={option.value}
- onChange={onChange}
- name={name}
- help={option.help}
- choices={option.choices}
- />
- </div>
- )
-}
diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx
index ef3a224a..5741ee8c 100644
--- a/web/src/js/components/Modal/OptionModal.jsx
+++ b/web/src/js/components/Modal/OptionModal.jsx
@@ -1,8 +1,22 @@
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
-import * as modalAction from '../../ducks/ui/modal'
-import { update as updateOptions } from '../../ducks/options'
-import Option from './OptionMaster'
+import React, { Component } from "react"
+import { connect } from "react-redux"
+import * as modalAction from "../../ducks/ui/modal"
+import Option from "./Option"
+
+function PureOptionHelp({help}){
+ return <div className="help-block small">{help}</div>;
+}
+const OptionHelp = connect((state, {name}) => ({
+ help: state.options[name].help,
+}))(PureOptionHelp);
+
+function PureOptionError({error}){
+ if(!error) return null;
+ return <div className="small text-danger">{error}</div>;
+}
+const OptionError = connect((state, {name}) => ({
+ error: state.ui.optionsEditor[name] && state.ui.optionsEditor[name].error
+}))(PureOptionError);
class PureOptionModal extends Component {
@@ -28,23 +42,25 @@ class PureOptionModal extends Component {
</div>
<div className="modal-body">
- {
- Object.keys(options).sort()
- .map((key, index) => {
- let option = options[key];
- return (
- <Option
- key={index}
- name={key}
- updateOptions={updateOptions}
- option={option}
- />)
- })
- }
+ <div className="form-horizontal">
+ {
+ options.map(name =>
+ <div key={name} className="form-group">
+ <div className="col-xs-6">
+ <label htmlFor={name}>{name}</label>
+ <OptionHelp name={name}/>
+ </div>
+ <div className="col-xs-6">
+ <Option name={name}/>
+ <OptionError name={name}/>
+ </div>
+ </div>
+ )
+ }
+ </div>
</div>
<div className="modal-footer">
- <button type="button" className="btn btn-primary">Save Changes</button>
</div>
</div>
)
@@ -53,10 +69,9 @@ class PureOptionModal extends Component {
export default connect(
state => ({
- options: state.options
+ options: Object.keys(state.options).sort()
}),
{
hideModal: modalAction.hideModal,
- updateOptions: updateOptions,
}
)(PureOptionModal)
diff --git a/web/src/js/ducks/connection.js b/web/src/js/ducks/connection.js
index ffa2c309..151277fb 100644
--- a/web/src/js/ducks/connection.js
+++ b/web/src/js/ducks/connection.js
@@ -1,6 +1,6 @@
export const ConnectionState = {
INIT: Symbol("init"),
- FETCHING: Symbol("fetching"), // WebSocket is established, but still startFetching resources.
+ FETCHING: Symbol("fetching"), // WebSocket is established, but still fetching resources.
ESTABLISHED: Symbol("established"),
ERROR: Symbol("error"),
OFFLINE: Symbol("offline"), // indicates that there is no live (websocket) backend.
diff --git a/web/src/js/ducks/options.js b/web/src/js/ducks/options.js
index 39c2f3fc..06144a3c 100644
--- a/web/src/js/ducks/options.js
+++ b/web/src/js/ducks/options.js
@@ -1,13 +1,12 @@
-import { fetchApi } from '../utils'
+import { fetchApi } from "../utils"
+import * as optionsEditorActions from "./ui/optionsEditor"
+import _ from "lodash"
-export const RECEIVE = 'OPTIONS_RECEIVE'
-export const UPDATE = 'OPTIONS_UPDATE'
+export const RECEIVE = 'OPTIONS_RECEIVE'
+export const UPDATE = 'OPTIONS_UPDATE'
export const REQUEST_UPDATE = 'REQUEST_UPDATE'
-export const UNKNOWN_CMD = 'OPTIONS_UNKNOWN_CMD'
-const defaultState = {
-
-}
+const defaultState = {}
export default function reducer(state = defaultState, action) {
switch (action.type) {
@@ -26,7 +25,22 @@ export default function reducer(state = defaultState, action) {
}
}
-export function update(options) {
- fetchApi.put('/options', options)
- return { type: REQUEST_UPDATE }
+export function pureSendUpdate (option, value, dispatch) {
+ fetchApi.put('/options', { [option]: value }).then(response => {
+ if (response.status === 200) {
+ dispatch(optionsEditorActions.updateSuccess(option))
+ } else {
+ response.text().then(error => {
+ dispatch(optionsEditorActions.updateError(option, error))
+ })
+ }
+ })
+}
+let sendUpdate = _.throttle(pureSendUpdate, 700, { leading: true, trailing: true })
+
+export function update(option, value) {
+ return dispatch => {
+ dispatch(optionsEditorActions.startUpdate(option, value))
+ sendUpdate(option, value, dispatch);
+ }
}
diff --git a/web/src/js/ducks/settings.js b/web/src/js/ducks/settings.js
index a2e360de..38c36842 100644
--- a/web/src/js/ducks/settings.js
+++ b/web/src/js/ducks/settings.js
@@ -3,7 +3,6 @@ import { fetchApi } from '../utils'
export const RECEIVE = 'SETTINGS_RECEIVE'
export const UPDATE = 'SETTINGS_UPDATE'
export const REQUEST_UPDATE = 'REQUEST_UPDATE'
-export const UNKNOWN_CMD = 'SETTINGS_UNKNOWN_CMD'
const defaultState = {
diff --git a/web/src/js/ducks/ui/index.js b/web/src/js/ducks/ui/index.js
index 741671b2..f5e6851f 100644
--- a/web/src/js/ducks/ui/index.js
+++ b/web/src/js/ducks/ui/index.js
@@ -2,10 +2,12 @@ import { combineReducers } from 'redux'
import flow from './flow'
import header from './header'
import modal from './modal'
+import optionsEditor from './optionsEditor'
// TODO: Just move ducks/ui/* into ducks/?
export default combineReducers({
flow,
header,
- modal
+ modal,
+ optionsEditor,
})
diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js
index 0e3491fa..e3f8c33c 100644
--- a/web/src/js/ducks/ui/keyboard.js
+++ b/web/src/js/ducks/ui/keyboard.js
@@ -1,6 +1,7 @@
import { Key } from "../../utils"
import { selectTab } from "./flow"
import * as flowsActions from "../flows"
+import * as modalActions from "./modal"
export function onKeyDown(e) {
@@ -46,7 +47,11 @@ export function onKeyDown(e) {
break
case Key.ESC:
- dispatch(flowsActions.select(null))
+ if(getState().ui.modal.activeModal){
+ dispatch(modalActions.hideModal())
+ } else {
+ dispatch(flowsActions.select(null))
+ }
break
case Key.LEFT: {
diff --git a/web/src/js/ducks/ui/optionsEditor.js b/web/src/js/ducks/ui/optionsEditor.js
new file mode 100644
index 00000000..a8a8f69e
--- /dev/null
+++ b/web/src/js/ducks/ui/optionsEditor.js
@@ -0,0 +1,73 @@
+import { HIDE_MODAL } from "./modal"
+
+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]: {
+ isUpdating: true,
+ value: action.value,
+ error: false,
+ }
+ }
+
+ case OPTION_UPDATE_SUCCESS:
+ return {
+ ...state,
+ [action.option]: undefined
+ }
+
+ case OPTION_UPDATE_ERROR:
+ let val = state[action.option].value;
+ if (typeof(val) === "boolean") {
+ // If a boolean option errs, reset it to its previous state to be less confusing.
+ // Example: Start mitmweb, check "add_upstream_certs_to_client_chain".
+ val = !val;
+ }
+ return {
+ ...state,
+ [action.option]: {
+ value: val,
+ isUpdating: false,
+ error: action.error
+ }
+ }
+
+ case HIDE_MODAL:
+ return {}
+
+ default:
+ return state
+ }
+}
+
+export function startUpdate(option, value) {
+ return {
+ type: OPTION_UPDATE_START,
+ option,
+ value,
+ }
+}
+export function updateSuccess(option) {
+ return {
+ type: OPTION_UPDATE_SUCCESS,
+ option,
+ }
+}
+
+export function updateError(option, error) {
+ return {
+ type: OPTION_UPDATE_ERROR,
+ option,
+ error,
+ }
+}