diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx new file mode 100644 index 000000000..1aca23c20 --- /dev/null +++ b/web/src/js/components/Modal/Option.jsx @@ -0,0 +1,121 @@ +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" + +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 ( + onChange(e.target.checked)} + {...props} + /> + ) +} + +StringOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +function StringOption({ value, onChange, ...props }) { + return ( + onChange(e.target.value)} + {...props} + /> + ) +} + +NumberOption.PropTypes = { + value: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, +} +function NumberOption({ value, onChange, ...props }) { + return ( + onChange(parseInt(e.target.value))} + {...props} + /> + ) +} + +ChoicesOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +function ChoicesOption({ value, onChange, choices, ...props }) { + return ( + onChange(e.target.value)} + selected={value} + {...props} + > + { choices.map( + choice => ( + {choice} + ) + )} + + ) +} + +StringSequenceOption.PropTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +} +function StringSequenceOption({ value, onChange, ...props }) { + const height = Math.max(value.length, 1) + return onChange(e.target.value.split("\n"))} + {...props} + /> +} + +const Options = { + "bool": BooleanOption, + "str": StringOption, + "int": NumberOption, + "optional str": StringOption, + "sequence of str": StringSequenceOption, +} + +function PureOption({ choices, type, value, onChange }) { + if (choices) { + return + } + const Opt = Options[type] + return +} +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 5befc34a8..000000000 --- a/web/src/js/components/Modal/OptionMaster.jsx +++ /dev/null @@ -1,257 +0,0 @@ -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 ( - - ) -} - -PureStringOption.PropTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -} - -function PureStringOption( { value, onChange, ...props }) { - let onKeyDown = (e) => {e.stopPropagation()} - return ( - - - - ) -} - -PureNumberOption.PropTypes = { - value: PropTypes.number.isRequired, - onChange: PropTypes.func.isRequired, -} - -function PureNumberOption( {value, onChange, ...props }) { - let onKeyDown = (e) => {e.stopPropagation()} - - return ( - - ) -} - -PureChoicesOption.PropTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, -} - -function PureChoicesOption( { value, onChange, name, choices, ...props}) { - return ( - - { choices.map((choice, index) => ( - {choice} - ))} - - ) -} - -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 ( - - - - ) - } -} - -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 ( - - - {name} - - - - {(focus || mousefocus) && ( - - - {option.help} - - )} - - - ) - } -} - -export default connect( - state => ({ - client_options: state.ui.option - }), - { - updateOptions - } -)(OptionMaster) diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index a4dd95d05..d16c2afcc 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -1,7 +1,22 @@ -import React, { Component } from 'react' -import { connect } from 'react-redux' -import * as modalAction from '../../ducks/ui/modal' -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 {help}; +} +const OptionHelp = connect((state, {name}) => ({ + help: state.options[name].help, +}))(PureOptionHelp); + +function PureOptionError({error}){ + if(!error) return null; + return {error}; +} +const OptionError = connect((state, {name}) => ({ + error: state.ui.optionsEditor[name] && state.ui.optionsEditor[name].error +}))(PureOptionError); class PureOptionModal extends Component { @@ -28,18 +43,20 @@ class PureOptionModal extends Component { - { - Object.keys(options).sort() - .map((key, index) => { - let option = options[key]; - return ( - ) - }) - } + { + options.map(name => + + + {name} + + + + + + + + ) + } @@ -52,7 +69,7 @@ class PureOptionModal extends Component { export default connect( state => ({ - options: state.options + options: Object.keys(state.options) }), { hideModal: modalAction.hideModal, diff --git a/web/src/js/ducks/connection.js b/web/src/js/ducks/connection.js index ffa2c3090..151277fbd 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 48e3708b9..286a1ae35 100644 --- a/web/src/js/ducks/options.js +++ b/web/src/js/ducks/options.js @@ -1,14 +1,12 @@ -import { fetchApi } from '../utils' -import * as optionActions from './ui/option' +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) { @@ -27,18 +25,23 @@ export default function reducer(state = defaultState, action) { } } -export function update(options) { + +let sendUpdate = (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)) + }) + } + }) +} +sendUpdate = _.throttle(sendUpdate, 700, { leading: true, trailing: true }) + +export function update(option, value) { return dispatch => { - let option = Object.keys(options)[0] - dispatch({ type: optionActions.OPTION_UPDATE_START, option, value: options[option] }) - fetchApi.put('/options', options).then(response => { - if (response.status === 200) { - dispatch({ type: optionActions.OPTION_UPDATE_SUCCESS, option}) - } else { - response.text().then( text => { - dispatch({type: optionActions.OPTION_UPDATE_ERROR, error: text, option}) - }) - } - }) + 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 a2e360de1..38c368426 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 cdee7ebbf..f5e6851fe 100644 --- a/web/src/js/ducks/ui/index.js +++ b/web/src/js/ducks/ui/index.js @@ -2,12 +2,12 @@ import { combineReducers } from 'redux' import flow from './flow' import header from './header' import modal from './modal' -import option from './option' +import optionsEditor from './optionsEditor' // TODO: Just move ducks/ui/* into ducks/? export default combineReducers({ flow, header, modal, - option, + optionsEditor, }) diff --git a/web/src/js/ducks/ui/keyboard.js b/web/src/js/ducks/ui/keyboard.js index 0e3491fab..e3f8c33c7 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/option.js b/web/src/js/ducks/ui/option.js deleted file mode 100644 index 6aba49989..000000000 --- a/web/src/js/ducks/ui/option.js +++ /dev/null @@ -1,39 +0,0 @@ -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 - } -} diff --git a/web/src/js/ducks/ui/optionsEditor.js b/web/src/js/ducks/ui/optionsEditor.js new file mode 100644 index 000000000..23dfe01a3 --- /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]: { + isUpdate: 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, + } +}