diff options
author | Matthew Shao <me@matshao.com> | 2017-07-01 08:46:04 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-07-01 08:46:04 -0500 |
commit | f3231ed758324a7de465ee5a377f9c40b0a8df34 (patch) | |
tree | 3979c67de95b398e8d0c89034feee60a0d0a3bb6 | |
parent | 321352ef0bc1911d49c8b6f8537674b03fa92f70 (diff) | |
parent | aad0b95cbe65e97574d49f3933002d347470d1ef (diff) | |
download | mitmproxy-f3231ed758324a7de465ee5a377f9c40b0a8df34.tar.gz mitmproxy-f3231ed758324a7de465ee5a377f9c40b0a8df34.tar.bz2 mitmproxy-f3231ed758324a7de465ee5a377f9c40b0a8df34.zip |
Merge pull request #2416 from MatthewShao/mitmweb-options
[WIP] [web] Mitmweb options editor UI
-rw-r--r-- | web/src/css/modal.less | 3 | ||||
-rw-r--r-- | web/src/js/__tests__/components/Header/FileMenuSpec.js | 15 | ||||
-rw-r--r-- | web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap | 13 | ||||
-rw-r--r-- | web/src/js/__tests__/components/Modal/ModalSpec.js | 30 | ||||
-rw-r--r-- | web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap | 66 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/optionsSpec.js | 25 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/tutils.js | 6 | ||||
-rw-r--r-- | web/src/js/__tests__/ducks/ui/modalSpec.js | 25 | ||||
-rw-r--r-- | web/src/js/components/Header/FileMenu.jsx | 12 | ||||
-rw-r--r-- | web/src/js/components/Modal/Modal.jsx | 24 | ||||
-rw-r--r-- | web/src/js/components/Modal/ModalLayout.jsx | 16 | ||||
-rw-r--r-- | web/src/js/components/Modal/ModalList.jsx | 13 | ||||
-rw-r--r-- | web/src/js/components/Modal/OptionModal.jsx | 45 | ||||
-rw-r--r-- | web/src/js/components/ProxyApp.jsx | 2 | ||||
-rw-r--r-- | web/src/js/ducks/options.js | 32 | ||||
-rw-r--r-- | web/src/js/ducks/ui/index.js | 2 | ||||
-rw-r--r-- | web/src/js/ducks/ui/modal.js | 33 |
17 files changed, 358 insertions, 4 deletions
diff --git a/web/src/css/modal.less b/web/src/css/modal.less new file mode 100644 index 00000000..b08e309a --- /dev/null +++ b/web/src/css/modal.less @@ -0,0 +1,3 @@ +.modal-visible { + display: block; +} diff --git a/web/src/js/__tests__/components/Header/FileMenuSpec.js b/web/src/js/__tests__/components/Header/FileMenuSpec.js index 65b4647a..0d87530b 100644 --- a/web/src/js/__tests__/components/Header/FileMenuSpec.js +++ b/web/src/js/__tests__/components/Header/FileMenuSpec.js @@ -8,13 +8,20 @@ describe('FileMenu Component', () => { let clearFn = jest.fn(), loadFn = jest.fn(), saveFn = jest.fn(), + openModalFn = jest.fn(), mockEvent = { preventDefault: jest.fn(), target: { files: ["foo", "bar "] } }, createNodeMock = () => { return { click: jest.fn() }}, fileMenu = renderer.create( - <FileMenu clearFlows={clearFn} loadFlows={loadFn} saveFlows={saveFn}/>, { createNodeMock }), + <FileMenu + clearFlows={clearFn} + loadFlows={loadFn} + saveFlows={saveFn} + openModal={openModalFn} + />, + { createNodeMock }), tree = fileMenu.toJSON() it('should render correctly', () => { @@ -42,4 +49,10 @@ describe('FileMenu Component', () => { a.props.onClick(mockEvent) expect(saveFn).toBeCalled() }) + + it('should open optionModal', () => { + let a = ul.children[3].children[1] + a.props.onClick(mockEvent) + expect(openModalFn).toBeCalled() + }) }) diff --git a/web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap b/web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap index 13ecf3f5..15c1afbc 100644 --- a/web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap +++ b/web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap @@ -63,6 +63,19 @@ exports[`FileMenu Component should render correctly 1`] = ` </li> <li> + <a + href="#" + onClick={[Function]} + > + <i + className="fa fa-fw fa-cog" + /> + Options + </a> + + </li> + <li> + <hr className="divider" /> diff --git a/web/src/js/__tests__/components/Modal/ModalSpec.js b/web/src/js/__tests__/components/Modal/ModalSpec.js new file mode 100644 index 00000000..e4880d08 --- /dev/null +++ b/web/src/js/__tests__/components/Modal/ModalSpec.js @@ -0,0 +1,30 @@ +import React from 'react' +import renderer from 'react-test-renderer' +import Modal from '../../../components/Modal/Modal' +import { Provider } from 'react-redux' +import { TStore } from '../../ducks/tutils' + +describe('Modal Component', () => { + let store = TStore() + + it('should render correctly', () => { + // hide modal by default + let provider = renderer.create( + <Provider store={store}> + <Modal/> + </Provider> + ), + tree = provider.toJSON() + expect(tree).toMatchSnapshot() + + // option modal show up + store.getState().ui.modal.activeModal = 'OptionModal' + provider = renderer.create( + <Provider store={store}> + <Modal/> + </Provider> + ) + tree = provider.toJSON() + expect(tree).toMatchSnapshot() + }) +}) diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap new file mode 100644 index 00000000..4fe163d1 --- /dev/null +++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Modal Component should render correctly 1`] = `<div />`; + +exports[`Modal Component should render correctly 2`] = ` +<div> + <div + className="modal-backdrop fade in" + /> + <div + aria-labelledby="options" + className="modal modal-visible" + id="optionsModal" + role="dialog" + tabIndex="-1" + > + <div + className="modal-dialog modal-lg" + role="document" + > + <div + className="modal-content" + > + <div> + <div + className="modal-header" + > + <button + className="close" + data-dismiss="modal" + onClick={[Function]} + type="button" + > + <i + className="fa fa-fw fa-times" + /> + </button> + <div + className="modal-title" + > + <h4> + Options + </h4> + </div> + </div> + <div + className="modal-body" + > + ... + </div> + <div + className="modal-footer" + > + <button + className="btn btn-primary" + type="button" + > + Save Changes + </button> + </div> + </div> + </div> + </div> + </div> +</div> +`; diff --git a/web/src/js/__tests__/ducks/optionsSpec.js b/web/src/js/__tests__/ducks/optionsSpec.js new file mode 100644 index 00000000..62019715 --- /dev/null +++ b/web/src/js/__tests__/ducks/optionsSpec.js @@ -0,0 +1,25 @@ +jest.mock('../../utils') + +import reduceOptions, * as OptionsActions from '../../ducks/options' + +describe('option reducer', () => { + it('should return initial state', () => { + expect(reduceOptions(undefined, {})).toEqual({}) + }) + + it('should handle receive action', () => { + let action = { type: OptionsActions.RECEIVE, data: 'foo' } + expect(reduceOptions(undefined, action)).toEqual('foo') + }) + + it('should handle update action', () => { + let action = {type: OptionsActions.UPDATE, data: {id: 1} } + expect(reduceOptions(undefined, action)).toEqual({id: 1}) + }) +}) + +describe('option actions', () => { + it('should be possible to update option', () => { + expect(reduceOptions(undefined, OptionsActions.update())).toEqual({}) + }) +}) diff --git a/web/src/js/__tests__/ducks/tutils.js b/web/src/js/__tests__/ducks/tutils.js index 2a79ede0..9b92e676 100644 --- a/web/src/js/__tests__/ducks/tutils.js +++ b/web/src/js/__tests__/ducks/tutils.js @@ -32,6 +32,9 @@ export function TStore(){ }, header: { tab: 'Start' + }, + modal: { + activeModal: undefined } }, settings: { @@ -47,7 +50,8 @@ export function TStore(){ sort: { desc: true, column: 'PathColumn' - } + }, + view: [ tflow ] }, connection: { state: ConnectionState.ESTABLISHED diff --git a/web/src/js/__tests__/ducks/ui/modalSpec.js b/web/src/js/__tests__/ducks/ui/modalSpec.js new file mode 100644 index 00000000..30c39760 --- /dev/null +++ b/web/src/js/__tests__/ducks/ui/modalSpec.js @@ -0,0 +1,25 @@ +import reduceModal, * as ModalActions from '../../../ducks/ui/modal' + +describe('modal reducer', () => { + let state = undefined + + it('should return the initial state', () => { + expect(reduceModal(undefined, {})).toEqual( + { activeModal: undefined } + ) + }) + + it('should handle setActiveModal action', () => { + state = reduceModal(undefined, ModalActions.setActiveModal('foo')) + expect(state).toEqual( + { activeModal: 'foo' } + ) + }) + + it('should handle hideModal action', () => { + state = reduceModal(state, ModalActions.hideModal()) + expect(state).toEqual( + { activeModal: undefined } + ) + }) +}) diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx index 70fbb2c3..62f721cf 100644 --- a/web/src/js/components/Header/FileMenu.jsx +++ b/web/src/js/components/Header/FileMenu.jsx @@ -4,11 +4,13 @@ import { connect } from 'react-redux' import FileChooser from '../common/FileChooser' import Dropdown, {Divider} from '../common/Dropdown' import * as flowsActions from '../../ducks/flows' +import * as modalActions from '../../ducks/ui/modal' FileMenu.propTypes = { clearFlows: PropTypes.func.isRequired, loadFlows: PropTypes.func.isRequired, - saveFlows: PropTypes.func.isRequired + saveFlows: PropTypes.func.isRequired, + openModal: PropTypes.func.isRequired, } FileMenu.onNewClick = (e, clearFlows) => { @@ -17,7 +19,7 @@ FileMenu.onNewClick = (e, clearFlows) => { clearFlows() } -export function FileMenu ({clearFlows, loadFlows, saveFlows}) { +export function FileMenu ({clearFlows, loadFlows, saveFlows, openModal}) { return ( <Dropdown className="pull-left" btnClass="special" text="mitmproxy"> <a href="#" onClick={e => FileMenu.onNewClick(e, clearFlows)}> @@ -34,6 +36,11 @@ export function FileMenu ({clearFlows, loadFlows, saveFlows}) { Save... </a> + <a href="#" onClick={e => { e.preventDefault(); openModal(); }}> + <i className="fa fa-fw fa-cog"></i> + Options + </a> + <Divider/> <a href="http://mitm.it/" target="_blank"> @@ -50,5 +57,6 @@ export default connect( clearFlows: flowsActions.clear, loadFlows: flowsActions.upload, saveFlows: flowsActions.download, + openModal: () => modalActions.setActiveModal('OptionModal'), } )(FileMenu) diff --git a/web/src/js/components/Modal/Modal.jsx b/web/src/js/components/Modal/Modal.jsx new file mode 100644 index 00000000..88e81156 --- /dev/null +++ b/web/src/js/components/Modal/Modal.jsx @@ -0,0 +1,24 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import ModalList from './ModalList' + +class PureModal extends Component { + + constructor(props, context) { + super(props, context) + } + + render() { + const { activeModal } = this.props + const ActiveModal = ModalList.find(m => m.name === activeModal ) + return( + activeModal ? <ActiveModal/> : <div/> + ) + } +} + +export default connect( + state => ({ + activeModal: state.ui.modal.activeModal + }) +)(PureModal) diff --git a/web/src/js/components/Modal/ModalLayout.jsx b/web/src/js/components/Modal/ModalLayout.jsx new file mode 100644 index 00000000..cf357b2b --- /dev/null +++ b/web/src/js/components/Modal/ModalLayout.jsx @@ -0,0 +1,16 @@ +import React from 'react' + +export default function ModalLayout ({ children }) { + return ( + <div> + <div className="modal-backdrop fade in"></div> + <div className="modal modal-visible" id="optionsModal" tabIndex="-1" role="dialog" aria-labelledby="options"> + <div className="modal-dialog modal-lg" role="document"> + <div className="modal-content"> + {children} + </div> + </div> + </div> + </div> + ) +} diff --git a/web/src/js/components/Modal/ModalList.jsx b/web/src/js/components/Modal/ModalList.jsx new file mode 100644 index 00000000..1175d5ea --- /dev/null +++ b/web/src/js/components/Modal/ModalList.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import ModalLayout from './ModalLayout' +import OptionContent from './OptionModal' + +function OptionModal() { + return ( + <ModalLayout> + <OptionContent/> + </ModalLayout> + ) +} + +export default [ OptionModal ] diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx new file mode 100644 index 00000000..500495c4 --- /dev/null +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -0,0 +1,45 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import * as modalAction from '../../ducks/ui/modal' + +class PureOptionModal extends Component { + + constructor(props, context) { + super(props, context) + this.state = { title: 'Options', } + } + + render() { + const { hideModal } = this.props + const { title } = this.state + return ( + <div> + <div className="modal-header"> + <button type="button" className="close" data-dismiss="modal" onClick={() => { + hideModal() + }}> + <i className="fa fa-fw fa-times"></i> + </button> + <div className="modal-title"> + <h4>{ title }</h4> + </div> + </div> + + <div className="modal-body"> + ... + </div> + + <div className="modal-footer"> + <button type="button" className="btn btn-primary">Save Changes</button> + </div> + </div> + ) + } +} + +export default connect( + state => ({ + + }), + { hideModal: modalAction.hideModal } +)(PureOptionModal) diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx index af5b3caa..8c3970bc 100644 --- a/web/src/js/components/ProxyApp.jsx +++ b/web/src/js/components/ProxyApp.jsx @@ -7,6 +7,7 @@ import MainView from './MainView' import Header from './Header' import EventLog from './EventLog' import Footer from './Footer' +import Modal from './Modal/Modal' class ProxyAppMain extends Component { @@ -28,6 +29,7 @@ class ProxyAppMain extends Component { <EventLog key="eventlog"/> )} <Footer /> + <Modal/> </div> ) } diff --git a/web/src/js/ducks/options.js b/web/src/js/ducks/options.js new file mode 100644 index 00000000..39c2f3fc --- /dev/null +++ b/web/src/js/ducks/options.js @@ -0,0 +1,32 @@ +import { fetchApi } from '../utils' + +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 = { + +} + +export default function reducer(state = defaultState, action) { + switch (action.type) { + + case RECEIVE: + return action.data + + case UPDATE: + return { + ...state, + ...action.data, + } + + default: + return state + } +} + +export function update(options) { + fetchApi.put('/options', options) + return { type: REQUEST_UPDATE } +} diff --git a/web/src/js/ducks/ui/index.js b/web/src/js/ducks/ui/index.js index 1d989eb1..741671b2 100644 --- a/web/src/js/ducks/ui/index.js +++ b/web/src/js/ducks/ui/index.js @@ -1,9 +1,11 @@ import { combineReducers } from 'redux' import flow from './flow' import header from './header' +import modal from './modal' // TODO: Just move ducks/ui/* into ducks/? export default combineReducers({ flow, header, + modal }) diff --git a/web/src/js/ducks/ui/modal.js b/web/src/js/ducks/ui/modal.js new file mode 100644 index 00000000..aafddaf7 --- /dev/null +++ b/web/src/js/ducks/ui/modal.js @@ -0,0 +1,33 @@ +export const HIDE_MODAL = 'UI_HIDE_MODAL' +export const SET_ACTIVE_MODAL = 'UI_SET_ACTIVE_MODAL' + +const defaultState = { + activeModal: undefined, +} + +export default function reducer(state = defaultState, action){ + switch (action.type){ + + case SET_ACTIVE_MODAL: + return { + ...state, + activeModal: action.activeModal, + } + + case HIDE_MODAL: + return { + ...state, + activeModal: undefined + } + default: + return state + } +} + +export function setActiveModal(activeModal) { + return { type: SET_ACTIVE_MODAL, activeModal } +} + +export function hideModal(){ + return { type: HIDE_MODAL } +} |