aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Shao <me@matshao.com>2017-07-01 08:46:04 -0500
committerGitHub <noreply@github.com>2017-07-01 08:46:04 -0500
commitf3231ed758324a7de465ee5a377f9c40b0a8df34 (patch)
tree3979c67de95b398e8d0c89034feee60a0d0a3bb6
parent321352ef0bc1911d49c8b6f8537674b03fa92f70 (diff)
parentaad0b95cbe65e97574d49f3933002d347470d1ef (diff)
downloadmitmproxy-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.less3
-rw-r--r--web/src/js/__tests__/components/Header/FileMenuSpec.js15
-rw-r--r--web/src/js/__tests__/components/Header/__snapshots__/FileMenuSpec.js.snap13
-rw-r--r--web/src/js/__tests__/components/Modal/ModalSpec.js30
-rw-r--r--web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap66
-rw-r--r--web/src/js/__tests__/ducks/optionsSpec.js25
-rw-r--r--web/src/js/__tests__/ducks/tutils.js6
-rw-r--r--web/src/js/__tests__/ducks/ui/modalSpec.js25
-rw-r--r--web/src/js/components/Header/FileMenu.jsx12
-rw-r--r--web/src/js/components/Modal/Modal.jsx24
-rw-r--r--web/src/js/components/Modal/ModalLayout.jsx16
-rw-r--r--web/src/js/components/Modal/ModalList.jsx13
-rw-r--r--web/src/js/components/Modal/OptionModal.jsx45
-rw-r--r--web/src/js/components/ProxyApp.jsx2
-rw-r--r--web/src/js/ducks/options.js32
-rw-r--r--web/src/js/ducks/ui/index.js2
-rw-r--r--web/src/js/ducks/ui/modal.js33
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}) {
&nbsp;Save...
</a>
+ <a href="#" onClick={e => { e.preventDefault(); openModal(); }}>
+ <i className="fa fa-fw fa-cog"></i>
+ &nbsp;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 }
+}