Merge pull request #2416 from MatthewShao/mitmweb-options

[WIP] [web] Mitmweb options editor UI
This commit is contained in:
Matthew Shao 2017-07-01 08:46:04 -05:00 committed by GitHub
commit f3231ed758
17 changed files with 358 additions and 4 deletions

3
web/src/css/modal.less Normal file
View File

@ -0,0 +1,3 @@
.modal-visible {
display: block;
}

View File

@ -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()
})
})

View File

@ -60,6 +60,19 @@ exports[`FileMenu Component should render correctly 1`] = `
 Save...
</a>
</li>
<li>
<a
href="#"
onClick={[Function]}
>
<i
className="fa fa-fw fa-cog"
/>
 Options
</a>
</li>
<li>

View File

@ -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()
})
})

View File

@ -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>
`;

View File

@ -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({})
})
})

View File

@ -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

View File

@ -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 }
)
})
})

View File

@ -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)

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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 ]

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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 }
}

View File

@ -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
})

View File

@ -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 }
}