diff options
author | Maximilian Hils <git@maximilianhils.com> | 2016-08-22 20:52:03 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-08-22 20:52:03 -0700 |
commit | 53ccbaf4f50d6876e1f4d44acdac2268b3d75233 (patch) | |
tree | 80b8037a85bf3d46e7a64782aa0539ee04be3a46 /web | |
parent | 62ab2f2fd5188f31e99560dce788ba85c2933a1a (diff) | |
parent | eddc4243791c2c2b1a91c1d8ae49b830206bc6df (diff) | |
download | mitmproxy-53ccbaf4f50d6876e1f4d44acdac2268b3d75233.tar.gz mitmproxy-53ccbaf4f50d6876e1f4d44acdac2268b3d75233.tar.bz2 mitmproxy-53ccbaf4f50d6876e1f4d44acdac2268b3d75233.zip |
Merge pull request #1489 from mitmproxy/web_refactor
Web refactor
Diffstat (limited to 'web')
-rw-r--r-- | web/src/css/dropdown.less | 4 | ||||
-rw-r--r-- | web/src/js/components/ContentView/CodeEditor.jsx | 3 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ContentViews.jsx | 33 | ||||
-rw-r--r-- | web/src/js/components/ContentView/UploadContentButton.jsx | 24 | ||||
-rw-r--r-- | web/src/js/components/ContentView/ViewSelector.jsx | 80 | ||||
-rw-r--r-- | web/src/js/components/Footer.jsx | 33 | ||||
-rw-r--r-- | web/src/js/components/Header/FileMenu.jsx | 133 | ||||
-rw-r--r-- | web/src/js/components/Header/FlowMenu.jsx | 6 | ||||
-rw-r--r-- | web/src/js/components/Header/OptionMenu.jsx | 6 | ||||
-rw-r--r-- | web/src/js/components/common/Dropdown.jsx | 53 | ||||
-rw-r--r-- | web/src/js/components/common/FileChooser.jsx | 27 | ||||
-rw-r--r-- | web/src/js/components/common/ToggleInputButton.jsx | 30 | ||||
-rw-r--r-- | web/src/js/ducks/flows.js | 12 | ||||
-rw-r--r-- | web/src/js/ducks/ui/flow.js | 3 | ||||
-rw-r--r-- | web/src/js/utils.js | 7 |
15 files changed, 231 insertions, 223 deletions
diff --git a/web/src/css/dropdown.less b/web/src/css/dropdown.less new file mode 100644 index 00000000..ba8442df --- /dev/null +++ b/web/src/css/dropdown.less @@ -0,0 +1,4 @@ +hr.divider { + margin-top: 5px; + margin-bottom: 5px; +} diff --git a/web/src/js/components/ContentView/CodeEditor.jsx b/web/src/js/components/ContentView/CodeEditor.jsx index d0430e6f..8afc128f 100644 --- a/web/src/js/components/ContentView/CodeEditor.jsx +++ b/web/src/js/components/ContentView/CodeEditor.jsx @@ -1,5 +1,4 @@ -import React, { Component, PropTypes } from 'react' -import { render } from 'react-dom'; +import React, {PropTypes} from 'react' import Codemirror from 'react-codemirror'; diff --git a/web/src/js/components/ContentView/ContentViews.jsx b/web/src/js/components/ContentView/ContentViews.jsx index cd593023..32a07564 100644 --- a/web/src/js/components/ContentView/ContentViews.jsx +++ b/web/src/js/components/ContentView/ContentViews.jsx @@ -30,6 +30,12 @@ function Edit({ content, onChange }) { Edit = ContentLoader(Edit) class ViewServer extends Component { + static propTypes = { + showFullContent: PropTypes.bool.isRequired, + maxLines: PropTypes.number.isRequired, + setContentViewDescription : PropTypes.func.isRequired, + setContent: PropTypes.func.isRequired + } componentWillMount(){ this.setContentView(this.props) @@ -40,6 +46,7 @@ class ViewServer extends Component { this.setContentView(nextProps) } } + setContentView(props){ try { this.data = JSON.parse(props.content) @@ -50,25 +57,31 @@ class ViewServer extends Component { props.setContentViewDescription(props.contentView != this.data.description ? this.data.description : '') props.setContent(this.data.lines) } + render() { const {content, contentView, message, maxLines} = this.props let lines = this.props.showFullContent ? this.data.lines : this.data.lines.slice(0, maxLines) - return <div> + return ( + <div> <pre> {lines.map((line, i) => <div key={`line${i}`}> - {line.map((tuple, j) => - <span key={`tuple${j}`} className={tuple[0]}> - {tuple[1]} - </span> - )} + {line.map((element, j) => { + let [style, text] = element + return ( + <span key={`tuple${j}`} className={style}> + {text} + </span> + ) + })} </div> )} </pre> - {ViewImage.matches(message) && - <ViewImage {...this.props} /> - } - </div> + {ViewImage.matches(message) && + <ViewImage {...this.props} /> + } + </div> + ) } } diff --git a/web/src/js/components/ContentView/UploadContentButton.jsx b/web/src/js/components/ContentView/UploadContentButton.jsx index 0652b584..de349af4 100644 --- a/web/src/js/components/ContentView/UploadContentButton.jsx +++ b/web/src/js/components/ContentView/UploadContentButton.jsx @@ -1,28 +1,18 @@ import { PropTypes } from 'react' +import FileChooser from '../common/FileChooser' UploadContentButton.propTypes = { uploadContent: PropTypes.func.isRequired, } export default function UploadContentButton({ uploadContent }) { - - let fileInput; - + return ( - <a className="btn btn-default btn-xs" - onClick={() => fileInput.click()} - title="Upload a file to replace the content."> - <i className="fa fa-upload"/> - <input - ref={ref => fileInput = ref} - className="hidden" - type="file" - onChange={e => { - if (e.target.files.length > 0) uploadContent(e.target.files[0]) - }} - /> - </a> - + <FileChooser + icon="fa-upload" + title="Upload a file to replace the content." + onOpenFile={uploadContent} + className="btn btn-default btn-xs"/> ) } diff --git a/web/src/js/components/ContentView/ViewSelector.jsx b/web/src/js/components/ContentView/ViewSelector.jsx index 59ec4276..ab433ea3 100644 --- a/web/src/js/components/ContentView/ViewSelector.jsx +++ b/web/src/js/components/ContentView/ViewSelector.jsx @@ -1,72 +1,36 @@ import React, { PropTypes, Component } from 'react' -import classnames from 'classnames' import { connect } from 'react-redux' import * as ContentViews from './ContentViews' -import { setContentView } from "../../ducks/ui/flow"; - -function ViewItem({ name, setContentView, children }) { - return ( - <li> - <a href="#" onClick={() => setContentView(name)}> - {children} - </a> - </li> - ) -} +import { setContentView } from '../../ducks/ui/flow'; +import Dropdown from '../common/Dropdown' -/*ViewSelector.propTypes = { +ViewSelector.propTypes = { contentViews: PropTypes.array.isRequired, activeView: PropTypes.string.isRequired, isEdit: PropTypes.bool.isRequired, - isContentViewSelectorOpen: PropTypes.bool.isRequired, - setContentViewSelectorOpen: PropTypes.func.isRequired -}*/ - - -class ViewSelector extends Component { - constructor(props, context) { - super(props, context) - this.close = this.close.bind(this) - this.state = {open: false} - } - close() { - this.setState({open: false}) - document.removeEventListener('click', this.close) - } - - onDropdown(e){ - e.preventDefault() - this.setState({open: !this.state.open}) - document.addEventListener('click', this.close) - } + setContentView: PropTypes.func.isRequired +} - render() { - const {contentViews, activeView, isEdit, setContentView} = this.props - let edit = ContentViews.Edit.displayName +function ViewSelector ({contentViews, activeView, isEdit, setContentView}){ + let edit = ContentViews.Edit.displayName + let inner = <span> <b>View:</b> {activeView}<span className="caret"></span> </span> - return ( - <div className={classnames('dropup pull-left', { open: this.state.open })}> - <a className="btn btn-default btn-xs" - onClick={ e => this.onDropdown(e) } - href="#"> - <b>View:</b> {activeView}<span className="caret"></span> + return ( + <Dropdown dropup className="pull-left" btnClass="btn btn-default btn-xs" text={inner}> + {contentViews.map(name => + <a href="#" key={name} onClick={e => {e.preventDefault(); setContentView(name)}}> + {name.toLowerCase().replace('_', ' ')} </a> - <ul className="dropdown-menu" role="menu"> - {contentViews.map(name => - <ViewItem key={name} setContentView={setContentView} name={name}> - {name.toLowerCase().replace('_', ' ')} - </ViewItem> - )} - {isEdit && - <ViewItem key={edit} setContentView={setContentView} name={edit}> - {edit.toLowerCase()} - </ViewItem> - } - </ul> - </div> - ) - } + ) + } + {isEdit && + <a href="#" onClick={e => {e.preventDefault(); setContentView(edit)}}> + {edit.toLowerCase()} + </a> + } + </Dropdown> + ) } export default connect ( diff --git a/web/src/js/components/Footer.jsx b/web/src/js/components/Footer.jsx index 2bda70e1..96e7b7db 100644 --- a/web/src/js/components/Footer.jsx +++ b/web/src/js/components/Footer.jsx @@ -7,40 +7,41 @@ Footer.propTypes = { } function Footer({ settings }) { + let {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickyauth, stickycookie, stream} = settings; return ( <footer> - {settings.mode && settings.mode != "regular" && ( - <span className="label label-success">{settings.mode} mode</span> + {mode && mode != "regular" && ( + <span className="label label-success">{mode} mode</span> )} - {settings.intercept && ( - <span className="label label-success">Intercept: {settings.intercept}</span> + {intercept && ( + <span className="label label-success">Intercept: {intercept}</span> )} - {settings.showhost && ( + {showhost && ( <span className="label label-success">showhost</span> )} - {settings.no_upstream_cert && ( + {no_upstream_cert && ( <span className="label label-success">no-upstream-cert</span> )} - {settings.rawtcp && ( + {rawtcp && ( <span className="label label-success">raw-tcp</span> )} - {!settings.http2 && ( + {!http2 && ( <span className="label label-success">no-http2</span> )} - {settings.anticache && ( + {anticache && ( <span className="label label-success">anticache</span> )} - {settings.anticomp && ( + {anticomp && ( <span className="label label-success">anticomp</span> )} - {settings.stickyauth && ( - <span className="label label-success">stickyauth: {settings.stickyauth}</span> + {stickyauth && ( + <span className="label label-success">stickyauth: {stickyauth}</span> )} - {settings.stickycookie && ( - <span className="label label-success">stickycookie: {settings.stickycookie}</span> + {stickycookie && ( + <span className="label label-success">stickycookie: {stickycookie}</span> )} - {settings.stream && ( - <span className="label label-success">stream: {formatSize(settings.stream)}</span> + {stream && ( + <span className="label label-success">stream: {formatSize(stream)}</span> )} </footer> ) diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx index d3786475..53c63ea1 100644 --- a/web/src/js/components/Header/FileMenu.jsx +++ b/web/src/js/components/Header/FileMenu.jsx @@ -1,103 +1,46 @@ -import React, { Component } from 'react' +import React, { Component, PropTypes } from 'react' import { connect } from 'react-redux' -import classnames from 'classnames' +import FileChooser from '../common/FileChooser' +import Dropdown, {Divider} from '../common/Dropdown' import * as flowsActions from '../../ducks/flows' -class FileMenu extends Component { - - constructor(props, context) { - super(props, context) - this.state = { show: false } - - this.close = this.close.bind(this) - this.onFileClick = this.onFileClick.bind(this) - this.onNewClick = this.onNewClick.bind(this) - this.onOpenClick = this.onOpenClick.bind(this) - this.onOpenFile = this.onOpenFile.bind(this) - this.onSaveClick = this.onSaveClick.bind(this) - } - - close() { - this.setState({ show: false }) - document.removeEventListener('click', this.close) - } - - onFileClick(e) { - e.preventDefault() - - if (this.state.show) { - return - } - - document.addEventListener('click', this.close) - this.setState({ show: true }) - } - - onNewClick(e) { - e.preventDefault() - if (confirm('Delete all flows?')) { - this.props.clearFlows() - } - } - - onOpenClick(e) { - e.preventDefault() - this.fileInput.click() - } - - onOpenFile(e) { - e.preventDefault() - if (e.target.files.length > 0) { - this.props.loadFlows(e.target.files[0]) - this.fileInput.value = '' - } - } +FileMenu.propTypes = { + clearFlows: PropTypes.func.isRequired, + loadFlows: PropTypes.func.isRequired, + saveFlows: PropTypes.func.isRequired +} - onSaveClick(e) { - e.preventDefault() - this.props.saveFlows() - } +FileMenu.onNewClick = (e, clearFlows) => { + e.preventDefault(); + if (confirm('Delete all flows?')) + clearFlows() +} - render() { - return ( - <div className={classnames('dropdown pull-left', { open: this.state.show })}> - <a href="#" className="special" onClick={this.onFileClick}>mitmproxy</a> - <ul className="dropdown-menu" role="menu"> - <li> - <a href="#" onClick={this.onNewClick}> - <i className="fa fa-fw fa-file"></i> - New - </a> - </li> - <li> - <a href="#" onClick={this.onOpenClick}> - <i className="fa fa-fw fa-folder-open"></i> - Open... - </a> - <input - ref={ref => this.fileInput = ref} - className="hidden" - type="file" - onChange={this.onOpenFile} - /> - </li> - <li> - <a href="#" onClick={this.onSaveClick}> - <i className="fa fa-fw fa-floppy-o"></i> - Save... - </a> - </li> - <li role="presentation" className="divider"></li> - <li> - <a href="http://mitm.it/" target="_blank"> - <i className="fa fa-fw fa-external-link"></i> - Install Certificates... - </a> - </li> - </ul> - </div> - ) - } +function FileMenu ({clearFlows, loadFlows, saveFlows}) { + return ( + <Dropdown className="pull-left" btnClass="special" text="mitmproxy"> + <a href="#" onClick={e => FileMenu.onNewClick(e, clearFlows)}> + <i className="fa fa-fw fa-file"></i> + New + </a> + <FileChooser + icon="fa-folder-open" + text="Open..." + onOpenFile={file => loadFlows(file)} + /> + <a href="#" onClick={e =>{ e.preventDefault(); saveFlows();}}> + <i className="fa fa-fw fa-floppy-o"></i> + Save... + </a> + + <Divider/> + + <a href="http://mitm.it/" target="_blank"> + <i className="fa fa-fw fa-external-link"></i> + Install Certificates... + </a> + </Dropdown> + ) } export default connect( diff --git a/web/src/js/components/Header/FlowMenu.jsx b/web/src/js/components/Header/FlowMenu.jsx index bdd30d5e..e78a49aa 100644 --- a/web/src/js/components/Header/FlowMenu.jsx +++ b/web/src/js/components/Header/FlowMenu.jsx @@ -8,10 +8,14 @@ FlowMenu.title = 'Flow' FlowMenu.propTypes = { flow: PropTypes.object.isRequired, + acceptFlow: PropTypes.func.isRequired, + replayFlow: PropTypes.func.isRequired, + duplicateFlow: PropTypes.func.isRequired, + removeFlow: PropTypes.func.isRequired, + revertFlow: PropTypes.func.isRequired } function FlowMenu({ flow, acceptFlow, replayFlow, duplicateFlow, removeFlow, revertFlow }) { - return ( <div> <div className="menu-row"> diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx index a338fed0..a11062f2 100644 --- a/web/src/js/components/Header/OptionMenu.jsx +++ b/web/src/js/components/Header/OptionMenu.jsx @@ -41,17 +41,17 @@ function OptionMenu({ settings, updateSettings }) { /> <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter" checked={!!settings.stickyauth} - txt={settings.stickyauth || ''} + txt={settings.stickyauth} onToggleChanged={txt => updateSettings({ stickyauth: !settings.stickyauth ? txt : null })} /> <ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter" checked={!!settings.stickycookie} - txt={settings.stickycookie || ''} + txt={settings.stickycookie} onToggleChanged={txt => updateSettings({ stickycookie: !settings.stickycookie ? txt : null })} /> <ToggleInputButton name="stream" placeholder="stream..." checked={!!settings.stream} - txt={settings.stream || ''} + txt={settings.stream} inputType="number" onToggleChanged={txt => updateSettings({ stream: !settings.stream ? txt : null })} /> diff --git a/web/src/js/components/common/Dropdown.jsx b/web/src/js/components/common/Dropdown.jsx new file mode 100644 index 00000000..cc95a6dc --- /dev/null +++ b/web/src/js/components/common/Dropdown.jsx @@ -0,0 +1,53 @@ +import React, { Component, PropTypes } from 'react' +import classnames from 'classnames' + +export const Divider = () => <hr className="divider"/> + +export default class Dropdown extends Component { + + static propTypes = { + dropup: PropTypes.bool, + className: PropTypes.string, + btnClass: PropTypes.string.isRequired + } + + static defaultProps = { + dropup: false + } + + constructor(props, context) { + super(props, context) + this.state = { open: false } + this.close = this.close.bind(this) + this.open = this.open.bind(this) + } + + close() { + this.setState({ open: false }) + document.removeEventListener('click', this.close) + } + + open(e){ + e.preventDefault() + if (this.state.open) { + return + } + this.setState({open: !this.state.open}) + document.addEventListener('click', this.close) + } + + render() { + const {dropup, className, btnClass, text, children} = this.props + return ( + <div className={classnames( (dropup ? 'dropup' : 'dropdown'), className, { open: this.state.open })}> + <a href='#' className={btnClass} + onClick={this.open}> + {text} + </a> + <ul className="dropdown-menu" role="menu"> + {children.map ( (item, i) => <li key={i}> {item} </li> )} + </ul> + </div> + ) + } +} diff --git a/web/src/js/components/common/FileChooser.jsx b/web/src/js/components/common/FileChooser.jsx new file mode 100644 index 00000000..d59d2d6d --- /dev/null +++ b/web/src/js/components/common/FileChooser.jsx @@ -0,0 +1,27 @@ +import React, { PropTypes } from 'react' + +FileChooser.propTypes = { + icon: PropTypes.string, + text: PropTypes.string, + className: PropTypes.string, + title: PropTypes.string, + onOpenFile: PropTypes.func.isRequired +} + +export default function FileChooser({ icon, text, className, title, onOpenFile }) { + let fileInput; + return ( + <a href='#' onClick={() => fileInput.click()} + className={className} + title={title}> + <i className={'fa fa-fw ' + icon}></i> + {text} + <input + ref={ref => fileInput = ref} + className="hidden" + type="file" + onChange={e => { e.preventDefault(); if(e.target.files.length > 0) onOpenFile(e.target.files[0]); fileInput = "";}} + /> + </a> + ) +} diff --git a/web/src/js/components/common/ToggleInputButton.jsx b/web/src/js/components/common/ToggleInputButton.jsx index 25d620ae..5fa24c10 100644 --- a/web/src/js/components/common/ToggleInputButton.jsx +++ b/web/src/js/components/common/ToggleInputButton.jsx @@ -6,17 +6,16 @@ export default class ToggleInputButton extends Component { static propTypes = { name: PropTypes.string.isRequired, - txt: PropTypes.string.isRequired, - onToggleChanged: PropTypes.func.isRequired + txt: PropTypes.string, + onToggleChanged: PropTypes.func.isRequired, + checked: PropTypes.bool.isRequired, + placeholder: PropTypes.string.isRequired, + inputType: PropTypes.string } constructor(props) { super(props) - this.state = { txt: props.txt } - } - - onChange(e) { - this.setState({ txt: e.target.value }) + this.state = { txt: props.txt || '' } } onKeyDown(e) { @@ -27,23 +26,24 @@ export default class ToggleInputButton extends Component { } render() { + const {checked, onToggleChanged, name, inputType, placeholder} = this.props return ( <div className="input-group toggle-input-btn"> <span className="input-group-btn" - onClick={() => this.props.onToggleChanged(this.state.txt)}> - <div className={classnames('btn', this.props.checked ? 'btn-primary' : 'btn-default')}> - <span className={classnames('fa', this.props.checked ? 'fa-check-square-o' : 'fa-square-o')}/> + onClick={() => onToggleChanged(this.state.txt)}> + <div className={classnames('btn', checked ? 'btn-primary' : 'btn-default')}> + <span className={classnames('fa', checked ? 'fa-check-square-o' : 'fa-square-o')}/> - {this.props.name} + {name} </div> </span> <input className="form-control" - placeholder={this.props.placeholder} - disabled={this.props.checked} + placeholder={placeholder} + disabled={checked} value={this.state.txt} - type={this.props.inputType} - onChange={e => this.onChange(e)} + type={inputType || 'text'} + onChange={e => this.setState({ txt: e.target.value })} onKeyDown={e => this.onKeyDown(e)} /> </div> diff --git a/web/src/js/ducks/flows.js b/web/src/js/ducks/flows.js index f96653a9..404db0d1 100644 --- a/web/src/js/ducks/flows.js +++ b/web/src/js/ducks/flows.js @@ -1,5 +1,6 @@ import { fetchApi } from '../utils' import reduceList, * as listActions from './utils/list' +import { selectRelative } from './flowView' import * as msgQueueActions from './msgQueue' import * as websocketActions from './websocket' @@ -210,5 +211,14 @@ export function updateFlow(item) { * @private */ export function removeFlow(id) { - return { type: REMOVE, id } + return (dispatch, getState) => { + let currentIndex = getState().flowView.indexOf[getState().flows.selected[0]] + let maxIndex = getState().flowView.data.length - 1 + let deleteLastEntry = maxIndex == 0 + if (deleteLastEntry) + dispatch(select()) + else + dispatch(selectRelative(currentIndex == maxIndex ? -1 : 1) ) + dispatch({ type: REMOVE, id }) + } } diff --git a/web/src/js/ducks/ui/flow.js b/web/src/js/ducks/ui/flow.js index 0360321c..4a6d64cd 100644 --- a/web/src/js/ducks/ui/flow.js +++ b/web/src/js/ducks/ui/flow.js @@ -149,6 +149,5 @@ export function setContent(content){ } export function stopEdit(flow, modifiedFlow) { - let diff = getDiff(flow, modifiedFlow) - return flowsActions.update(flow, diff) + return flowsActions.update(flow, getDiff(flow, modifiedFlow)) } diff --git a/web/src/js/utils.js b/web/src/js/utils.js index e44182d0..e8470cec 100644 --- a/web/src/js/utils.js +++ b/web/src/js/utils.js @@ -107,14 +107,15 @@ fetchApi.put = (url, json, options) => fetchApi( ...options } ) - +// deep comparison of two json objects (dicts). arrays are handeled as a single value. +// return: json object including only the changed keys value pairs. export function getDiff(obj1, obj2) { let result = {...obj2}; for(let key in obj1) { if(_.isEqual(obj2[key], obj1[key])) result[key] = undefined - else if(!(Array.isArray(obj2[key]) && Array.isArray(obj1[key])) && - typeof obj2[key] == 'object' && typeof obj1[key] == 'object') + else if(Object.prototype.toString.call(obj2[key]) === '[object Object]' && + Object.prototype.toString.call(obj1[key]) === '[object Object]' ) result[key] = getDiff(obj1[key], obj2[key]) } return result |