diff options
Diffstat (limited to 'web/src/js/components/header.js')
-rw-r--r-- | web/src/js/components/header.js | 399 |
1 files changed, 399 insertions, 0 deletions
diff --git a/web/src/js/components/header.js b/web/src/js/components/header.js new file mode 100644 index 00000000..998a41df --- /dev/null +++ b/web/src/js/components/header.js @@ -0,0 +1,399 @@ +var React = require("react"); +var $ = require("jquery"); + +var Filt = require("../filt/filt.js"); +var utils = require("../utils.js"); +var common = require("./common.js"); +var actions = require("../actions.js"); +var Query = require("../actions.js").Query; + +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 <i className="fa fa-spinner fa-spin"></i>; + } else { + var commands = FilterDocs.doc.commands.map(function (c) { + return <tr key={c[1]}> + <td>{c[0].replace(" ", '\u00a0')}</td> + <td>{c[1]}</td> + </tr>; + }); + commands.push(<tr key="docs-link"> + <td colSpan="2"> + <a href="https://mitmproxy.org/doc/features/filters.html" + target="_blank"> + <i className="fa fa-external-link"></i> + mitmproxy docs</a> + </td> + </tr>); + return <table className="table table-condensed"> + <tbody>{commands}</tbody> + </table>; + } + } +}); +var FilterInput = React.createClass({ + mixins: [common.ChildFocus], + 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 { + Filt.parse(filt || this.state.value); + return true; + } catch (e) { + return false; + } + }, + getDesc: function () { + var desc; + try { + desc = Filt.parse(this.state.value).desc; + } catch (e) { + desc = "" + e; + } + if (desc !== "true") { + return desc; + } else { + return ( + <FilterDocs/> + ); + } + }, + 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 === utils.Key.ESC || e.keyCode === utils.Key.ENTER) { + this.blur(); + // If closed using ESC/ENTER, hide the tooltip. + this.setState({mousefocus: false}); + } + e.stopPropagation(); + }, + blur: function () { + this.refs.input.getDOMNode().blur(); + this.returnFocus(); + }, + select: function () { + this.refs.input.getDOMNode().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 = ( + <div className="popover bottom" onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}> + <div className="arrow"></div> + <div className="popover-content"> + {this.getDesc()} + </div> + </div> + ); + } + + return ( + <div className={groupClassName}> + <span className="input-group-addon"> + <i className={icon} style={{color: this.props.color}}></i> + </span> + <input type="text" placeholder={this.props.placeholder} className="form-control" + ref="input" + onChange={this.onChange} + onFocus={this.onFocus} + onBlur={this.onBlur} + onKeyDown={this.onKeyDown} + value={this.state.value}/> + {popover} + </div> + ); + } +}); + +var MainMenu = React.createClass({ + mixins: [common.Navigation, common.RouterState, common.SettingsState], + statics: { + title: "Start", + route: "flows" + }, + onSearchChange: function (val) { + var d = {}; + d[Query.SEARCH] = val; + this.setQuery(d); + }, + onHighlightChange: function (val) { + var d = {}; + d[Query.HIGHLIGHT] = val; + this.setQuery(d); + }, + onInterceptChange: function (val) { + actions.SettingsActions.update({intercept: val}); + }, + render: function () { + var search = this.getQuery()[Query.SEARCH] || ""; + var highlight = this.getQuery()[Query.HIGHLIGHT] || ""; + var intercept = this.state.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> + ); + } +}); + + +var ViewMenu = React.createClass({ + statics: { + title: "View", + route: "flows" + }, + mixins: [common.Navigation, common.RouterState], + toggleEventLog: function () { + var d = {}; + + if (this.getQuery()[Query.SHOW_EVENTLOG]) { + d[Query.SHOW_EVENTLOG] = undefined; + } else { + d[Query.SHOW_EVENTLOG] = "t"; // any non-false value will do it, keep it short + } + + this.setQuery(d); + }, + render: function () { + var showEventLog = this.getQuery()[Query.SHOW_EVENTLOG]; + return ( + <div> + <button + className={"btn " + (showEventLog ? "btn-primary" : "btn-default")} + onClick={this.toggleEventLog}> + <i className="fa fa-database"></i> + Show Eventlog + </button> + <span> </span> + </div> + ); + } +}); + + +var ReportsMenu = React.createClass({ + statics: { + title: "Visualization", + route: "reports" + }, + render: function () { + return <div>Reports Menu</div>; + } +}); + +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?")) { + actions.FlowActions.clear(); + } + }, + handleOpenClick: function (e) { + e.preventDefault(); + console.error("unimplemented: handleOpenClick"); + }, + handleSaveClick: function (e) { + e.preventDefault(); + console.error("unimplemented: handleSaveClick"); + }, + handleShutdownClick: function (e) { + e.preventDefault(); + console.error("unimplemented: handleShutdownClick"); + }, + render: function () { + var fileMenuClass = "dropdown pull-left" + (this.state.showFileMenu ? " open" : ""); + + return ( + <div className={fileMenuClass}> + <a href="#" className="special" onClick={this.handleFileClick}> mitmproxy </a> + <ul className="dropdown-menu" role="menu"> + <li> + <a href="#" onClick={this.handleNewClick}> + <i className="fa fa-fw fa-file"></i> + New + </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> + {/* + <li> + <a href="#" onClick={this.handleOpenClick}> + <i className="fa fa-fw fa-folder-open"></i> + Open + </a> + </li> + <li> + <a href="#" onClick={this.handleSaveClick}> + <i className="fa fa-fw fa-save"></i> + Save + </a> + </li> + <li role="presentation" className="divider"></li> + <li> + <a href="#" onClick={this.handleShutdownClick}> + <i className="fa fa-fw fa-plug"></i> + Shutdown + </a> + </li> + */} + </ul> + </div> + ); + } +}); + + +var header_entries = [MainMenu, ViewMenu /*, ReportsMenu */]; + + +var Header = React.createClass({ + mixins: [common.Navigation], + getInitialState: function () { + return { + active: header_entries[0] + }; + }, + handleClick: function (active, e) { + e.preventDefault(); + this.replaceWith(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 ( + <a key={i} + href="#" + className={className} + onClick={this.handleClick.bind(this, entry)}> + { entry.title} + </a> + ); + }.bind(this)); + + return ( + <header> + <nav className="nav-tabs nav-tabs-lg"> + <FileMenu/> + {header} + </nav> + <div className="menu"> + <this.state.active ref="active"/> + </div> + </header> + ); + } +}); + + +module.exports = { + Header: Header, + MainMenu: MainMenu +};
\ No newline at end of file |