aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js
diff options
context:
space:
mode:
authorMaximilian Hils <git@maximilianhils.com>2017-07-17 20:46:28 +0200
committerMaximilian Hils <git@maximilianhils.com>2017-07-17 21:04:01 +0200
commitbabd967eb8ac62c4a6ff6734ff57e46faaa5bab6 (patch)
treec44148fd3fc96f94f76442d1c8bcb97e117e169b /web/src/js
parent21b3f9c02956c625c576c6787ab10409aab0618d (diff)
downloadmitmproxy-babd967eb8ac62c4a6ff6734ff57e46faaa5bab6.tar.gz
mitmproxy-babd967eb8ac62c4a6ff6734ff57e46faaa5bab6.tar.bz2
mitmproxy-babd967eb8ac62c4a6ff6734ff57e46faaa5bab6.zip
[web] options: make help and err permanently visible, improve perf
Diffstat (limited to 'web/src/js')
-rw-r--r--web/src/js/components/Modal/Option.jsx121
-rw-r--r--web/src/js/components/Modal/OptionMaster.jsx257
-rw-r--r--web/src/js/components/Modal/OptionModal.jsx51
-rw-r--r--web/src/js/ducks/connection.js2
-rw-r--r--web/src/js/ducks/options.js43
-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/option.js39
-rw-r--r--web/src/js/ducks/ui/optionsEditor.js73
10 files changed, 260 insertions, 338 deletions
diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx
new file mode 100644
index 00000000..1aca23c2
--- /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 (
+ <input type="checkbox"
+ checked={value}
+ onChange={e => onChange(e.target.checked)}
+ {...props}
+ />
+ )
+}
+
+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}
+ />
+ )
+}
+
+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,
+}
+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}
+ />
+}
+
+const Options = {
+ "bool": BooleanOption,
+ "str": StringOption,
+ "int": NumberOption,
+ "optional str": StringOption,
+ "sequence of str": StringSequenceOption,
+}
+
+function PureOption({ choices, type, value, onChange }) {
+ if (choices) {
+ return <ChoicesOption
+ value={value}
+ onChange={onChange}
+ choices={choices}
+ onKeyDown={stopPropagation}
+ />
+ }
+ const Opt = Options[type]
+ return <Opt
+ value={value}
+ onChange={onChange}
+ onKeyDown={stopPropagation}
+ />
+}
+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 5befc34a..00000000
--- 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 (
- <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/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx
index a4dd95d0..d16c2afc 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 <div className="small text-muted">{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,18 +43,20 @@ class PureOptionModal extends Component {
<div className="modal-body">
<div className="container-fluid">
- {
- Object.keys(options).sort()
- .map((key, index) => {
- let option = options[key];
- return (
- <Option
- key={index}
- name={key}
- option={option}
- />)
- })
- }
+ {
+ options.map(name =>
+ <div key={name} className="row">
+ <div className="col-xs-6">
+ {name}
+ <OptionHelp name={name}/>
+ </div>
+ <div className="col-xs-6">
+ <Option name={name}/>
+ <OptionError name={name}/>
+ </div>
+ </div>
+ )
+ }
</div>
</div>
@@ -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 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 48e3708b..286a1ae3 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 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 cdee7ebb..f5e6851f 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 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/option.js b/web/src/js/ducks/ui/option.js
deleted file mode 100644
index 6aba4998..00000000
--- 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 00000000..23dfe01a
--- /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,
+ }
+}