diff --git a/doc-src/index.py b/doc-src/index.py index e6064e3a4..9eb0ea2bd 100644 --- a/doc-src/index.py +++ b/doc-src/index.py @@ -40,29 +40,7 @@ def example(s): ns.example = example -filt_help = [] -for i in filt.filt_unary: - filt_help.append( - ("~%s"%i.code, i.help) - ) -for i in filt.filt_rex: - filt_help.append( - ("~%s regex"%i.code, i.help) - ) -for i in filt.filt_int: - filt_help.append( - ("~%s int"%i.code, i.help) - ) -filt_help.sort() -filt_help.extend( - [ - ("!", "unary not"), - ("&", "and"), - ("|", "or"), - ("(...)", "grouping"), - ] -) -ns.filt_help = filt_help +ns.filt_help = filt.help def nav(page, current, state): diff --git a/libmproxy/filt.py b/libmproxy/filt.py index 5d2590965..40b2f6c95 100644 --- a/libmproxy/filt.py +++ b/libmproxy/filt.py @@ -351,3 +351,26 @@ def parse(s): except ValueError: return None + +help = [] +for i in filt_unary: + help.append( + ("~%s"%i.code, i.help) + ) +for i in filt_rex: + help.append( + ("~%s regex"%i.code, i.help) + ) +for i in filt_int: + help.append( + ("~%s int"%i.code, i.help) + ) +help.sort() +help.extend( + [ + ("!", "unary not"), + ("&", "and"), + ("|", "or"), + ("(...)", "grouping"), + ] +) \ No newline at end of file diff --git a/libmproxy/web/app.py b/libmproxy/web/app.py index 27e9aefc6..31cbf2e22 100644 --- a/libmproxy/web/app.py +++ b/libmproxy/web/app.py @@ -4,7 +4,7 @@ import tornado.web import tornado.websocket import logging import json -from .. import version +from .. import version, filt class APIError(tornado.web.HTTPError): @@ -52,6 +52,12 @@ class IndexHandler(RequestHandler): self.render("index.html") +class FiltHelp(RequestHandler): + def get(self): + self.write(dict( + commands=filt.help + )) + class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): connections = None # raise an error if inherited class doesn't specify its own instance. @@ -194,6 +200,7 @@ class Application(tornado.web.Application): self.master = master handlers = [ (r"/", IndexHandler), + (r"/filter-help", FiltHelp), (r"/updates", ClientConnection), (r"/events", Events), (r"/flows", Flows), diff --git a/libmproxy/web/static/css/app.css b/libmproxy/web/static/css/app.css index 2ec275a31..554fffb18 100644 --- a/libmproxy/web/static/css/app.css +++ b/libmproxy/web/static/css/app.css @@ -138,10 +138,30 @@ header .menu { padding: 10px; border-bottom: solid #a6a6a6 1px; } +.menu-row { + margin-left: -2.5px; + margin-right: -2.5px; +} +.filter-input { + position: relative; + min-height: 1px; + padding-left: 2.5px; + padding-right: 2.5px; +} +@media (min-width: 992px) { + .filter-input { + float: left; + width: 25%; + } +} .filter-input .popover { top: 27px; display: block; - width: 100%; + max-width: none; +} +.filter-input .popover .popover-content { + max-height: 500px; + overflow-y: auto; } .flow-table { width: 100%; diff --git a/libmproxy/web/static/js/app.js b/libmproxy/web/static/js/app.js index 984db9433..c054baa28 100644 --- a/libmproxy/web/static/js/app.js +++ b/libmproxy/web/static/js/app.js @@ -2632,6 +2632,48 @@ var VirtualScrollMixin = { } }, }; +var FilterDocs = React.createClass({displayName: 'FilterDocs', + 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 React.createElement("i", {className: "fa fa-spinner fa-spin"}); + } else { + var commands = FilterDocs.doc.commands.map(function (c) { + return React.createElement("tr", null, + React.createElement("td", null, c[0].replace(" ", '\u00a0')), + React.createElement("td", null, c[1]) + ); + }); + commands.push(React.createElement("tr", null, + React.createElement("td", {colSpan: "2"}, + React.createElement("a", {href: "https://mitmproxy.org/doc/features/filters.html", + target: "_blank"}, + React.createElement("i", {className: "fa fa-external-link"}), + " mitmproxy docs") + ) + )); + return React.createElement("table", {className: "table table-condensed"}, + React.createElement("tbody", null, commands) + ); + } + } +}); var FilterInput = React.createClass({displayName: 'FilterInput', getInitialState: function () { // Consider both focus and mouseover for showing/hiding the tooltip, @@ -2675,10 +2717,7 @@ var FilterInput = React.createClass({displayName: 'FilterInput', return desc; } else { return ( - React.createElement("a", {href: "https://mitmproxy.org/doc/features/filters.html", target: "_blank"}, - React.createElement("i", {className: "fa fa-external-link"}), - "Filter Documentation" - ) + React.createElement(FilterDocs, null) ); } }, @@ -2768,28 +2807,27 @@ var MainMenu = React.createClass({displayName: 'MainMenu', return ( React.createElement("div", null, - React.createElement("form", {className: "form-inline", style: {display: "inline"}}, + React.createElement("div", {className: "menu-row"}, React.createElement(FilterInput, { placeholder: "Filter", type: "filter", color: "black", value: filter, onChange: this.onFilterChange}), - React.createElement("span", null, " "), React.createElement(FilterInput, { placeholder: "Highlight", type: "tag", color: "hsl(48, 100%, 50%)", value: highlight, onChange: this.onHighlightChange}), - React.createElement("span", null, " "), React.createElement(FilterInput, { placeholder: "Intercept", type: "pause", color: "hsl(208, 56%, 53%)", value: intercept, onChange: this.onInterceptChange}) - ) + ), + React.createElement("div", {className: "clearfix"}) ) ); } diff --git a/web/src/css/header.less b/web/src/css/header.less index ce85d5283..3bbd28334 100644 --- a/web/src/css/header.less +++ b/web/src/css/header.less @@ -1,21 +1,37 @@ +@import (reference) '../../bower_components/bootstrap/less/variables.less'; +@import (reference) '../../bower_components/bootstrap/less/mixins/grid.less'; + header { - background-color: white; + background-color: white; - .title-bar { - line-height: 25px; - text-align: center; - } + .title-bar { + line-height: 25px; + text-align: center; + } - @separator-color: lighten(grey, 15%); + @separator-color: lighten(grey, 15%); - .menu { - padding: 10px; - border-bottom: solid @separator-color 1px; - } + .menu { + padding: 10px; + border-bottom: solid @separator-color 1px; + } +} + +@menu-row-gutter-width: 5px; +.menu-row { + .make-row(@menu-row-gutter-width); +} + +.filter-input { + .make-md-column(3, @menu-row-gutter-width); } .filter-input .popover { - top: 27px; - display: block; - width: 100%; + top: 27px; + display: block; + max-width: none; + .popover-content { + max-height: 500px; + overflow-y: auto; + } } \ No newline at end of file diff --git a/web/src/js/components/header.jsx.js b/web/src/js/components/header.jsx.js index ba63f12e4..6470aec5e 100644 --- a/web/src/js/components/header.jsx.js +++ b/web/src/js/components/header.jsx.js @@ -1,3 +1,45 @@ +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