From 81a0c45c89df2dc94f7d97c4367f0e549495e4d0 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 9 Jun 2016 20:34:57 +0800 Subject: [web] header.js -> Header.js --- web/src/js/components/EventLog.jsx | 2 +- web/src/js/components/Header.js | 56 ++++ web/src/js/components/Header/FileMenu.jsx | 100 ++++++ web/src/js/components/Header/FilterDocs.jsx | 56 ++++ web/src/js/components/Header/FilterInput.jsx | 133 ++++++++ web/src/js/components/Header/MainMenu.jsx | 73 +++++ web/src/js/components/Header/OptionMenu.jsx | 60 ++++ web/src/js/components/Header/ViewMenu.jsx | 33 ++ web/src/js/components/ProxyApp.jsx | 4 +- web/src/js/components/header.js | 464 --------------------------- web/src/js/components/prompt.js | 4 +- 11 files changed, 516 insertions(+), 469 deletions(-) create mode 100644 web/src/js/components/Header.js create mode 100644 web/src/js/components/Header/FileMenu.jsx create mode 100644 web/src/js/components/Header/FilterDocs.jsx create mode 100644 web/src/js/components/Header/FilterInput.jsx create mode 100644 web/src/js/components/Header/MainMenu.jsx create mode 100644 web/src/js/components/Header/OptionMenu.jsx create mode 100644 web/src/js/components/Header/ViewMenu.jsx delete mode 100644 web/src/js/components/header.js (limited to 'web/src/js') diff --git a/web/src/js/components/EventLog.jsx b/web/src/js/components/EventLog.jsx index 3de38954..24b3c2bf 100644 --- a/web/src/js/components/EventLog.jsx +++ b/web/src/js/components/EventLog.jsx @@ -19,7 +19,7 @@ function EventLog({ filters, events, onToggleFilter, onClose }) { Eventlog
{['debug', 'info', 'web'].map(type => ( - onToggleFilter(type)}/> + onToggleFilter(type)}/> ))}
diff --git a/web/src/js/components/Header.js b/web/src/js/components/Header.js new file mode 100644 index 00000000..7134f7d9 --- /dev/null +++ b/web/src/js/components/Header.js @@ -0,0 +1,56 @@ +import React, { Component, PropTypes } from 'react' +import classnames from 'classnames' +import { toggleEventLogVisibility } from '../ducks/eventLog' +import MainMenu from './Header/MainMenu' +import ViewMenu from './Header/ViewMenu' +import OptionMenu from './Header/OptionMenu' +import FileMenu from './Header/FileMenu' + +export default class Header extends Component { + + static entries = [MainMenu, ViewMenu, OptionMenu] + + static propTypes = { + settings: PropTypes.object.isRequired, + } + + constructor(props, context) { + super(props, context) + this.state = { active: Header.entries[0] } + } + + handleClick(active, e) { + e.preventDefault() + this.props.updateLocation(active.route) + this.setState({ active }) + } + + render() { + const { active: Active } = this.state + const { settings, updateLocation, query } = this.props + + return ( +
+ +
+ +
+
+ ) + } +} 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 ( +
+ mitmproxy + +
+ ) + } +} 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 ? ( + + ) : ( + + + {doc.commands.map(cmd => ( + + + + + ))} + + + + +
{cmd[0].replace(' ', '\u00a0')}{cmd[1]}
+ + +   mitmproxy docs +
+ ) + } +} 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 + } + 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 ( +
+ + + + + {(focus || mousefocus) && ( +
+
+
+ {this.getDesc()} +
+
+ )} +
+ ) + } +} 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 ( +
+
+ + + +
+
+
+ ) + } +} 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 ( +
+
+ SettingsActions.update({ showhost: !settings.showhost })} + /> + SettingsActions.update({ no_upstream_cert: !settings.no_upstream_cert })} + /> + SettingsActions.update({ rawtcp: !settings.rawtcp })} + /> + SettingsActions.update({ http2: !settings.http2 })} + /> + SettingsActions.update({ anticache: !settings.anticache })} + /> + SettingsActions.update({ anticomp: !settings.anticomp })} + /> + SettingsActions.update({ stickyauth: !settings.stickyauth ? txt : null })} + /> + SettingsActions.update({ stickycookie: !settings.stickycookie ? txt : null })} + /> + SettingsActions.update({ stream: !settings.stream ? txt : null })} + /> +
+
+
+ ) +} 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 ( +
+
+ +
+
+
+ ) +} + +export default connect( + state => ({ + visible: state.eventLog.visible, + }), + dispatch => bindActionCreators({ + onToggle: toggleEventLogVisibility, + }, dispatch) +)(ViewMenu) diff --git a/web/src/js/components/ProxyApp.jsx b/web/src/js/components/ProxyApp.jsx index bab8183d..81272268 100644 --- a/web/src/js/components/ProxyApp.jsx +++ b/web/src/js/components/ProxyApp.jsx @@ -4,7 +4,7 @@ import _ from "lodash" import { connect } from 'react-redux' import { Splitter } from "./common.js" -import { Header, MainMenu } from "./header.js" +import Header from "./Header" import EventLog from "./EventLog" import Footer from "./Footer" import { SettingsStore } from "../store/store.js" @@ -134,7 +134,7 @@ class ProxyAppMain extends Component { if (name) { const headerComponent = this.refs.header - headerComponent.setState({ active: MainMenu }, () => { + headerComponent.setState({ active: Header.entries.MainMenu }, () => { headerComponent.refs.active.refs[name].select() }) } diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js deleted file mode 100644 index ebd77f91..00000000 --- a/web/src/js/components/header.js +++ /dev/null @@ -1,464 +0,0 @@ -import React from "react"; -import ReactDOM from 'react-dom'; -import { bindActionCreators } from 'redux' -import $ from "jquery"; -import {connect} from 'react-redux' - -import Filt from "../filt/filt.js"; -import {Key} from "../utils.js"; -import {ToggleInputButton, ToggleButton} from "./common.js"; -import {SettingsActions, FlowActions} from "../actions.js"; -import {Query} from "../actions.js"; -import {SettingsState} from "./common.js"; -import { toggleEventLogVisibility } from '../ducks/eventLog' - -const ToggleEventLog = connect( - state => ({ - checked: state.eventLog.visible - }), - dispatch => bindActionCreators({ - onToggle: toggleEventLogVisibility, - }, dispatch) -)(ToggleButton) - -var FilterDocs = React.createClass({ - statics: { - xhr: false, - doc: false - }, - componentWillMount: function () { - if (!FilterDocs.doc) { - FilterDocs.xhr = $.getJSON("/filter-help").done(function (doc) { - FilterDocs.doc = doc; - FilterDocs.xhr = false; - }); - } - if (FilterDocs.xhr) { - FilterDocs.xhr.done(function () { - this.forceUpdate(); - }.bind(this)); - } - }, - render: function () { - if (!FilterDocs.doc) { - return ; - } else { - var commands = FilterDocs.doc.commands.map(function (c) { - return - {c[0].replace(" ", '\u00a0')} - {c[1]} - ; - }); - commands.push( - - - -   mitmproxy docs - - ); - return - {commands} -
; - } - } -}); -var FilterInput = React.createClass({ - contextTypes: { - returnFocus: React.PropTypes.func - }, - getInitialState: function () { - // 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. - return { - value: this.props.value, - focus: false, - mousefocus: false - }; - }, - componentWillReceiveProps: function (nextProps) { - this.setState({value: nextProps.value}); - }, - onChange: function (e) { - var nextValue = e.target.value; - this.setState({ - value: nextValue - }); - // Only propagate valid filters upwards. - if (this.isValid(nextValue)) { - this.props.onChange(nextValue); - } - }, - isValid: function (filt) { - try { - var str = filt || this.state.value; - if(str){ - Filt.parse(filt || this.state.value); - } - return true; - } catch (e) { - return false; - } - }, - getDesc: function () { - if(this.state.value) { - try { - return Filt.parse(this.state.value).desc; - } catch (e) { - return "" + e; - } - } - return ; - }, - onFocus: function () { - this.setState({focus: true}); - }, - onBlur: function () { - this.setState({focus: false}); - }, - onMouseEnter: function () { - this.setState({mousefocus: true}); - }, - onMouseLeave: function () { - this.setState({mousefocus: false}); - }, - onKeyDown: function (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: function () { - ReactDOM.findDOMNode(this.refs.input).blur(); - this.context.returnFocus(); - }, - select: function () { - ReactDOM.findDOMNode(this.refs.input).select(); - }, - render: function () { - var isValid = this.isValid(); - var icon = "fa fa-fw fa-" + this.props.type; - var groupClassName = "filter-input input-group" + (isValid ? "" : " has-error"); - - var popover; - if (this.state.focus || this.state.mousefocus) { - popover = ( -
-
-
- {this.getDesc()} -
-
- ); - } - - return ( -
- - - - - {popover} -
- ); - } -}); - -export var MainMenu = React.createClass({ - propTypes: { - settings: React.PropTypes.object.isRequired, - }, - statics: { - title: "Start", - route: "flows" - }, - onSearchChange: function (val) { - var d = {}; - d[Query.SEARCH] = val; - this.props.updateLocation(undefined, d); - }, - onHighlightChange: function (val) { - var d = {}; - d[Query.HIGHLIGHT] = val; - this.props.updateLocation(undefined, d); - }, - onInterceptChange: function (val) { - SettingsActions.update({intercept: val}); - }, - render: function () { - var search = this.props.query[Query.SEARCH] || ""; - var highlight = this.props.query[Query.HIGHLIGHT] || ""; - var intercept = this.props.settings.intercept || ""; - - return ( -
-
- - - -
-
-
- ); - } -}); - - -var ViewMenu = React.createClass({ - statics: { - title: "View", - route: "flows" - }, - render: function () { - return ( -
-
- -
-
-
- ); - } -}); - -export const OptionMenu = (props) => { - const {mode, intercept, showhost, no_upstream_cert, rawtcp, http2, anticache, anticomp, stickycookie, stickyauth, stream} = props.settings; - return ( -
-
- SettingsActions.update({showhost: !showhost})} - /> - SettingsActions.update({no_upstream_cert: !no_upstream_cert})} - /> - SettingsActions.update({rawtcp: !rawtcp})} - /> - SettingsActions.update({http2: !http2})} - /> - SettingsActions.update({anticache: !anticache})} - /> - SettingsActions.update({anticomp: !anticomp})} - /> - SettingsActions.update({stickyauth: (!stickyauth ? txt : null)})} - /> - SettingsActions.update({stickycookie: (!stickycookie ? txt : null)})} - /> - SettingsActions.update({stream: (!stream ? txt : null)})} - /> -
-
-
- ); -}; -OptionMenu.title = "Options"; - -OptionMenu.propTypes = { - settings: React.PropTypes.object.isRequired -}; - -var ReportsMenu = React.createClass({ - statics: { - title: "Visualization", - route: "reports" - }, - render: function () { - return
Reports Menu
; - } -}); - -var FileMenu = React.createClass({ - getInitialState: function () { - return { - showFileMenu: false - }; - }, - handleFileClick: function (e) { - e.preventDefault(); - if (!this.state.showFileMenu) { - var close = function () { - this.setState({showFileMenu: false}); - document.removeEventListener("click", close); - }.bind(this); - document.addEventListener("click", close); - - this.setState({ - showFileMenu: true - }); - } - }, - handleNewClick: function (e) { - e.preventDefault(); - if (confirm("Delete all flows?")) { - FlowActions.clear(); - } - }, - handleOpenClick: function (e) { - this.fileInput.click(); - e.preventDefault(); - }, - handleOpenFile: function (e) { - if (e.target.files.length > 0) { - FlowActions.upload(e.target.files[0]); - this.fileInput.value = ""; - } - e.preventDefault(); - }, - handleSaveClick: function (e) { - e.preventDefault(); - FlowActions.download(); - }, - handleShutdownClick: function (e) { - e.preventDefault(); - console.error("unimplemented: handleShutdownClick"); - }, - render: function () { - var fileMenuClass = "dropdown pull-left" + (this.state.showFileMenu ? " open" : ""); - - return ( -
- mitmproxy - -
- ); - } -}); - - -var header_entries = [MainMenu, ViewMenu, OptionMenu /*, ReportsMenu */]; - - -export var Header = React.createClass({ - propTypes: { - settings: React.PropTypes.object.isRequired, - }, - getInitialState: function () { - return { - active: header_entries[0] - }; - }, - handleClick: function (active, e) { - e.preventDefault(); - this.props.updateLocation(active.route); - this.setState({active: active}); - }, - render: function () { - var header = header_entries.map(function (entry, i) { - var className; - if (entry === this.state.active) { - className = "active"; - } else { - className = ""; - } - return ( - - {entry.title} - - ); - }.bind(this)); - - return ( -
- -
- -
-
- ); - } -}); diff --git a/web/src/js/components/prompt.js b/web/src/js/components/prompt.js index e324f7d4..5ab26b82 100644 --- a/web/src/js/components/prompt.js +++ b/web/src/js/components/prompt.js @@ -42,7 +42,7 @@ var Prompt = React.createClass({ var opts = []; var keyTaken = function (k) { - return _.includes(_.pluck(opts, "key"), k); + return _.includes(_.map(opts, "key"), k); }; for (var i = 0; i < this.props.options.length; i++) { @@ -99,4 +99,4 @@ var Prompt = React.createClass({ } }); -export default Prompt; \ No newline at end of file +export default Prompt; -- cgit v1.2.3