aboutsummaryrefslogtreecommitdiffstats
path: root/web/src/js/components/Header
diff options
context:
space:
mode:
authorJason <jason.daurus@gmail.com>2016-06-09 20:34:57 +0800
committerJason <jason.daurus@gmail.com>2016-06-09 20:35:03 +0800
commit81a0c45c89df2dc94f7d97c4367f0e549495e4d0 (patch)
treeb9e824f77a7b15b5f3e1a2ab680e2eec5aabf8c8 /web/src/js/components/Header
parent6c95635cb809d9261acc317f223ef80ba9c25f20 (diff)
downloadmitmproxy-81a0c45c89df2dc94f7d97c4367f0e549495e4d0.tar.gz
mitmproxy-81a0c45c89df2dc94f7d97c4367f0e549495e4d0.tar.bz2
mitmproxy-81a0c45c89df2dc94f7d97c4367f0e549495e4d0.zip
[web] header.js -> Header.js
Diffstat (limited to 'web/src/js/components/Header')
-rw-r--r--web/src/js/components/Header/FileMenu.jsx100
-rw-r--r--web/src/js/components/Header/FilterDocs.jsx56
-rw-r--r--web/src/js/components/Header/FilterInput.jsx133
-rw-r--r--web/src/js/components/Header/MainMenu.jsx73
-rw-r--r--web/src/js/components/Header/OptionMenu.jsx60
-rw-r--r--web/src/js/components/Header/ViewMenu.jsx33
6 files changed, 455 insertions, 0 deletions
diff --git a/web/src/js/components/Header/FileMenu.jsx b/web/src/js/components/Header/FileMenu.jsx
new file mode 100644
index 00000000..b075b3c8
--- /dev/null
+++ b/web/src/js/components/Header/FileMenu.jsx
@@ -0,0 +1,100 @@
+import React, { Component } from 'react'
+import classnames from 'classnames'
+import { FlowActions } from '../../actions.js'
+
+export default 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?')) {
+ FlowActions.clear()
+ }
+ }
+
+ onOpenClick(e) {
+ e.preventDefault()
+ this.fileInput.click()
+ }
+
+ onOpenFile(e) {
+ e.preventDefault()
+ if (e.target.files.length > 0) {
+ FlowActions.upload(e.target.files[0])
+ this.fileInput.value = ''
+ }
+ }
+
+ onSaveClick(e) {
+ e.preventDefault()
+ FlowActions.download()
+ }
+
+ 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>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/FilterDocs.jsx b/web/src/js/components/Header/FilterDocs.jsx
new file mode 100644
index 00000000..efb4818c
--- /dev/null
+++ b/web/src/js/components/Header/FilterDocs.jsx
@@ -0,0 +1,56 @@
+import React, { Component } from 'react'
+import $ from 'jquery'
+
+export default class FilterDocs extends Component {
+
+ // @todo move to redux
+
+ static xhr = null
+ static doc = null
+
+ constructor(props, context) {
+ super(props, context)
+ this.state = { doc: FilterDocs.doc }
+ }
+
+ componentWillMount() {
+ if (!FilterDocs.xhr) {
+ FilterDocs.xhr = $.getJSON('/filter-help')
+ FilterDocs.xhr.fail(() => {
+ FilterDocs.xhr = null
+ })
+ }
+ if (!this.state.doc) {
+ FilterDocs.xhr.done(doc => {
+ FilterDocs.doc = doc
+ this.setState({ doc })
+ })
+ }
+ }
+
+ render() {
+ const { doc } = this.state
+ return !doc ? (
+ <i className="fa fa-spinner fa-spin"></i>
+ ) : (
+ <table className="table table-condensed">
+ <tbody>
+ {doc.commands.map(cmd => (
+ <tr key={cmd[1]}>
+ <td>{cmd[0].replace(' ', '\u00a0')}</td>
+ <td>{cmd[1]}</td>
+ </tr>
+ ))}
+ <tr key="docs-link">
+ <td colSpan="2">
+ <a href="http://docs.mitmproxy.org/en/stable/features/filters.html"
+ target="_blank">
+ <i className="fa fa-external-link"></i>
+ &nbsp mitmproxy docs</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/FilterInput.jsx b/web/src/js/components/Header/FilterInput.jsx
new file mode 100644
index 00000000..5b49b788
--- /dev/null
+++ b/web/src/js/components/Header/FilterInput.jsx
@@ -0,0 +1,133 @@
+import React, { PropTypes, Component } from 'react'
+import ReactDOM from 'react-dom'
+import classnames from 'classnames'
+import { Key } from '../../utils.js'
+import Filt from '../../filt/filt'
+import FilterDocs from './FilterDocs'
+
+export default class FilterInput extends Component {
+
+ static contextTypes = {
+ returnFocus: React.PropTypes.func,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+
+ // Consider both focus and mouseover for showing/hiding the tooltip,
+ // because onBlur of the input is triggered before the click on the tooltip
+ // finalized, hiding the tooltip just as the user clicks on it.
+ this.state = { value: this.props.value, focus: false, mousefocus: false }
+
+ this.onChange = this.onChange.bind(this)
+ this.onFocus = this.onFocus.bind(this)
+ this.onBlur = this.onBlur.bind(this)
+ this.onKeyDown = this.onKeyDown.bind(this)
+ this.onMouseEnter = this.onMouseEnter.bind(this)
+ this.onMouseLeave = this.onMouseLeave.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({ value: nextProps.value })
+ }
+
+ isValid(filt) {
+ try {
+ const str = filt == null ? this.state.value : filt
+ if (str) {
+ Filt.parse(str)
+ }
+ return true
+ } catch (e) {
+ return false
+ }
+ }
+
+ getDesc() {
+ if (!this.state.value) {
+ return <FilterDocs/>
+ }
+ try {
+ return Filt.parse(this.state.value).desc
+ } catch (e) {
+ return '' + e
+ }
+ }
+
+ onChange(e) {
+ const value = e.target.value
+ this.setState({ value })
+
+ // Only propagate valid filters upwards.
+ if (this.isValid(value)) {
+ this.props.onChange(value)
+ }
+ }
+
+ onFocus() {
+ this.setState({ focus: true })
+ }
+
+ onBlur() {
+ this.setState({ focus: false })
+ }
+
+ onMouseEnter() {
+ this.setState({ mousefocus: true })
+ }
+
+ onMouseLeave() {
+ this.setState({ mousefocus: false })
+ }
+
+ onKeyDown(e) {
+ if (e.keyCode === Key.ESC || e.keyCode === Key.ENTER) {
+ this.blur()
+ // If closed using ESC/ENTER, hide the tooltip.
+ this.setState({mousefocus: false})
+ }
+ e.stopPropagation()
+ }
+
+ blur() {
+ ReactDOM.findDOMNode(this.refs.input).blur()
+ this.context.returnFocus()
+ }
+
+ select() {
+ ReactDOM.findDOMNode(this.refs.input).select()
+ }
+
+ render() {
+ const { type, color, placeholder } = this.props
+ const { value, focus, mousefocus } = this.state
+ return (
+ <div className={classnames('filter-input input-group', { 'has-error': !this.isValid() })}>
+ <span className="input-group-addon">
+ <i className={'fa fa-fw fa-' + type} style={{ color }}></i>
+ </span>
+ <input
+ type="text"
+ ref="input"
+ placeholder={placeholder}
+ className="form-control"
+ value={value}
+ onChange={this.onChange}
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ onKeyDown={this.onKeyDown}
+ />
+ {(focus || mousefocus) && (
+ <div className="popover bottom"
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}>
+ <div className="arrow"></div>
+ <div className="popover-content">
+ {this.getDesc()}
+ </div>
+ </div>
+ )}
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/MainMenu.jsx b/web/src/js/components/Header/MainMenu.jsx
new file mode 100644
index 00000000..86bf961a
--- /dev/null
+++ b/web/src/js/components/Header/MainMenu.jsx
@@ -0,0 +1,73 @@
+import React, { Component, PropTypes } from 'react'
+import { SettingsActions } from "../../actions.js"
+import FilterInput from './FilterInput'
+import { Query } from '../../actions.js'
+
+export default class MainMenu extends Component {
+
+ static title = 'Start'
+ static route = 'flows'
+
+ static propTypes = {
+ settings: React.PropTypes.object.isRequired,
+ }
+
+ constructor(props, context) {
+ super(props, context)
+ this.onSearchChange = this.onSearchChange.bind(this)
+ this.onHighlightChange = this.onHighlightChange.bind(this)
+ this.onInterceptChange = this.onInterceptChange.bind(this)
+ }
+
+ onSearchChange(val) {
+ this.props.updateLocation(undefined, { [Query.SEARCH]: val })
+ }
+
+ onHighlightChange(val) {
+ this.props.updateLocation(undefined, { [Query.HIGHLIGHT]: val })
+ }
+
+ onInterceptChange(val) {
+ SettingsActions.update({ intercept: val })
+ }
+
+ render() {
+ const { query, settings } = this.props
+
+ const search = query[Query.SEARCH] || ''
+ const highlight = query[Query.HIGHLIGHT] || ''
+ const intercept = settings.intercept || ''
+
+ return (
+ <div>
+ <div className="menu-row">
+ <FilterInput
+ ref="search"
+ placeholder="Search"
+ type="search"
+ color="black"
+ value={search}
+ onChange={this.onSearchChange}
+ />
+ <FilterInput
+ ref="highlight"
+ placeholder="Highlight"
+ type="tag"
+ color="hsl(48, 100%, 50%)"
+ value={highlight}
+ onChange={this.onHighlightChange}
+ />
+ <FilterInput
+ ref="intercept"
+ placeholder="Intercept"
+ type="pause"
+ color="hsl(208, 56%, 53%)"
+ value={intercept}
+ onChange={this.onInterceptChange}
+ />
+ </div>
+ <div className="clearfix"></div>
+ </div>
+ )
+ }
+}
diff --git a/web/src/js/components/Header/OptionMenu.jsx b/web/src/js/components/Header/OptionMenu.jsx
new file mode 100644
index 00000000..6bbf15d5
--- /dev/null
+++ b/web/src/js/components/Header/OptionMenu.jsx
@@ -0,0 +1,60 @@
+import React, { PropTypes } from 'react'
+import { ToggleInputButton, ToggleButton } from '../common.js'
+import { SettingsActions } from '../../actions.js'
+
+OptionMenu.title = "Options"
+
+OptionMenu.propTypes = {
+ settings: PropTypes.object.isRequired,
+}
+
+export default function OptionMenu({ settings }) {
+ // @todo use settings.map
+ return (
+ <div>
+ <div className="menu-row">
+ <ToggleButton text="showhost"
+ checked={settings.showhost}
+ onToggle={() => SettingsActions.update({ showhost: !settings.showhost })}
+ />
+ <ToggleButton text="no_upstream_cert"
+ checked={settings.no_upstream_cert}
+ onToggle={() => SettingsActions.update({ no_upstream_cert: !settings.no_upstream_cert })}
+ />
+ <ToggleButton text="rawtcp"
+ checked={settings.rawtcp}
+ onToggle={() => SettingsActions.update({ rawtcp: !settings.rawtcp })}
+ />
+ <ToggleButton text="http2"
+ checked={settings.http2}
+ onToggle={() => SettingsActions.update({ http2: !settings.http2 })}
+ />
+ <ToggleButton text="anticache"
+ checked={settings.anticache}
+ onToggle={() => SettingsActions.update({ anticache: !settings.anticache })}
+ />
+ <ToggleButton text="anticomp"
+ checked={settings.anticomp}
+ onToggle={() => SettingsActions.update({ anticomp: !settings.anticomp })}
+ />
+ <ToggleInputButton name="stickyauth" placeholder="Sticky auth filter"
+ checked={!!settings.stickyauth}
+ txt={settings.stickyauth || ''}
+ onToggleChanged={txt => SettingsActions.update({ stickyauth: !settings.stickyauth ? txt : null })}
+ />
+ <ToggleInputButton name="stickycookie" placeholder="Sticky cookie filter"
+ checked={!!settings.stickycookie}
+ txt={settings.stickycookie || ''}
+ onToggleChanged={txt => SettingsActions.update({ stickycookie: !settings.stickycookie ? txt : null })}
+ />
+ <ToggleInputButton name="stream" placeholder="stream..."
+ checked={!!settings.stream}
+ txt={settings.stream || ''}
+ inputType="number"
+ onToggleChanged={txt => SettingsActions.update({ stream: !settings.stream ? txt : null })}
+ />
+ </div>
+ <div className="clearfix"/>
+ </div>
+ )
+}
diff --git a/web/src/js/components/Header/ViewMenu.jsx b/web/src/js/components/Header/ViewMenu.jsx
new file mode 100644
index 00000000..45359a83
--- /dev/null
+++ b/web/src/js/components/Header/ViewMenu.jsx
@@ -0,0 +1,33 @@
+import React, { PropTypes } from 'react'
+import { bindActionCreators } from 'redux'
+import { connect } from 'react-redux'
+import { ToggleButton } from '../common.js'
+import { toggleEventLogVisibility } from '../../ducks/eventLog'
+
+ViewMenu.title = 'View'
+ViewMenu.route = 'flows'
+
+ViewMenu.propTypes = {
+ visible: PropTypes.bool.isRequired,
+ onToggle: PropTypes.func.isRequired,
+}
+
+function ViewMenu({ visible, onToggle }) {
+ return (
+ <div>
+ <div className="menu-row">
+ <ToggleButton text="Show Event Log" checked={visible} onToggle={onToggle} />
+ </div>
+ <div className="clearfix"></div>
+ </div>
+ )
+}
+
+export default connect(
+ state => ({
+ visible: state.eventLog.visible,
+ }),
+ dispatch => bindActionCreators({
+ onToggle: toggleEventLogVisibility,
+ }, dispatch)
+)(ViewMenu)