web: start gui

This commit is contained in:
Maximilian Hils 2014-09-13 23:05:12 +02:00
parent 9bacb6d426
commit d2475e6a14
16 changed files with 5722 additions and 66 deletions

View File

@ -10,13 +10,15 @@
"qunit": "",
"benchmark": "",
"benchmarkjs-runner": "",
"bootstrap": ""
"bootstrap": "",
"react-bootstrap": ""
},
"install": {
"path": "src/vendor",
"sources": {
"lodash": "bower_components/lodash/dist/lodash.js",
"react": "bower_components/react/react-with-addons.js"
"react": "bower_components/react/react-with-addons.js",
"react-bootstrap": "bower_components/react-bootstrap/react-bootstrap.js"
}
}
}

View File

@ -6,6 +6,7 @@ var jshint = require("gulp-jshint");
var less = require("gulp-less");
var livereload = require("gulp-livereload");
var minifyCSS = require('gulp-minify-css');
var plumber = require("gulp-plumber");
var react = require("gulp-react");
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');
@ -19,11 +20,13 @@ var path = {
'vendor/lodash/lodash.js',
'vendor/react/react-with-addons.js',
'vendor/react-router/react-router.js',
'vendor/react-bootstrap/react-bootstrap.js'
],
app: [
'js/router.jsx',
'js/certinstall.jsx',
'js/mitmproxy.js',
'js/datastructures.js',
'js/footer.react.js',
'js/header.react.js',
'js/mitmproxy.react.js',
],
},
css: {
@ -40,6 +43,7 @@ gulp.task("fonts", function () {
function styles(files, dev) {
return (gulp.src(files, {base: "src", cwd: "src"})
.pipe(plumber())
.pipe(dev ? sourcemaps.init() : gutil.noop())
.pipe(less())
.pipe(dev ? sourcemaps.write(".", {sourceRoot: "/static"}) : gutil.noop())
@ -58,8 +62,9 @@ gulp.task("styles-prod", ["styles-app-prod", "styles-vendor-prod"]);
function scripts(files, filename, dev) {
return gulp.src(files, {base: "src", cwd: "src"})
.pipe(plumber())
.pipe(dev ? sourcemaps.init() : gutil.noop())
.pipe(react())
.pipe(react({harmony: true}))
.pipe(concat(filename))
.pipe(!dev ? uglify() : gutil.noop())
.pipe(dev ? sourcemaps.write(".", {sourceRoot: "/static"}) : gutil.noop())
@ -75,7 +80,8 @@ gulp.task("scripts-prod", ["scripts-app-prod", "scripts-vendor-prod"]);
gulp.task("jshint", function () {
return gulp.src(["src/js/**"])
.pipe(react())
.pipe(plumber())
.pipe(react({harmony: true}))
.pipe(jshint())
.pipe(jshint.reporter("jshint-stylish"))
});

View File

@ -22,6 +22,7 @@
"gulp-less": "^1.3.5",
"gulp-livereload": "^2.1.1",
"gulp-minify-css": "^0.3.8",
"gulp-plumber": "^0.6.5",
"gulp-react": "^1.0.1",
"gulp-sourcemaps": "^1.1.5",
"gulp-uglify": "^1.0.1",

View File

@ -1,8 +1,12 @@
.certinstall {
// www.paulirish.com/2012/box-sizing-border-box-ftw/
html {
box-sizing: border-box;
}
@color: white;
body {
background-color: @color;
}
*, *:before, *:after {
box-sizing: inherit;
}
@import (less) "layout.less";
@import (less) "header.less";
@import (less) "footer.less";

3
web/src/css/footer.less Normal file
View File

@ -0,0 +1,3 @@
footer {
padding: 0 10px;
}

62
web/src/css/header.less Normal file
View File

@ -0,0 +1,62 @@
header {
background-color: white;
.title-bar {
line-height: 25px;
text-align: center;
}
@separator-color: lighten(grey, 15%);
nav {
border-bottom: solid @separator-color 1px;
a {
display: inline-block;
padding: 3px 14px;
margin: 0 2px -1px;
border: solid transparent 1px;
//text-transform: uppercase;
//font-family: Lato;
&.active {
border-color: @separator-color;
border-bottom-color: white;
}
&:hover {
/*
@preview: lightgrey;
border-top-color: @preview;
border-left-color: @preview;
border-right-color: @preview;
*/
text-decoration: none;
}
&.special {
@special-color: #396cad;
color: white;
background-color: @special-color;
border-bottom-color: @special-color;
&:hover {
background-color: lighten(@special-color, 10%);
}
}
}
&:before {
content: " ";
}
&:after {
clear: both;
}
}
.menu {
height: 100px;
border-bottom: solid @separator-color 1px;
}
}

36
web/src/css/layout.less Normal file
View File

@ -0,0 +1,36 @@
html, body, #container {
height: 100%;
margin: 0;
overflow: hidden;
}
header, footer {
display: block;
}
@headerheight: 153px;
@footerheight: 25px;
#container {
//Set padding on container so that #main can take 100% height
//If we don't do it, the scrollbars will be too large.
padding: @headerheight 0 @footerheight;
}
header {
height: @headerheight;
//Substract #container padding
margin-top: -@headerheight;
}
#main {
height: 100%;
display: block;
overflow-y: auto;
}
footer {
//This starts at the beginning of the #container padding, all fine.
height: @footerheight;
line-height: @footerheight;
}

View File

@ -8,14 +8,11 @@
<link rel="stylesheet" href="css/app.css"/>
<script src="js/vendor.js"></script>
<script src="js/app.js"></script>
<script>
$(function () {
mitmproxy.init();
});
</script>
</head>
<body>
<div id="mitmproxy"></div>
</body>
<script>
app = React.renderComponent(routes, document.body);
</script>
</html>

View File

@ -1,27 +0,0 @@
/** @jsx React.DOM */
var CertInstallView = React.createClass({
render: function () {
return <div className="certinstall">
<h2> Click to install the mitmproxy certificate: </h2>
<div id="certbank" className="row">
<div className="col-md-3">
<a href="/cert/pem"><i className="fa fa-apple fa-5x"></i></a>
<p>Apple</p>
</div>
<div className="col-md-3">
<a href="/cert/p12"><i className="fa fa-windows fa-5x"></i></a>
<p>Windows</p>
</div>
<div className="col-md-3">
<a href="/cert/pem"><i className="fa fa-android fa-5x"></i></a>
<p>Android</p>
</div>
<div className="col-md-3">
<a href="/cert/pem"><i className="fa fa-asterisk fa-5x"></i></a>
<p>Other</p>
</div>
</div>
</div>;
}
});

View File

@ -0,0 +1,101 @@
class EventEmitter {
constructor(){
this._listeners = {};
}
emit(event){
if(!(event in this._listeners)){
return;
}
this._listeners[event].forEach(function (listener) {
listener(event, this);
}.bind(this));
}
addListener(event, f){
this._listeners[event] = this._listeners[event] || [];
this._listeners[event].push(f);
}
removeListener(event, f){
if(!(event in this._listeners)){
return false;
}
var index = this._listeners.indexOf(f);
if (index >= 0) {
this._listeners.splice(this._listeners.indexOf(f), 1);
}
}
}
var FLOW_CHANGED = "flow.changed";
class FlowStore extends EventEmitter{
constructor() {
super();
this.flows = [];
this._listeners = [];
}
getAll() {
return this.flows;
}
emitChange() {
return this.emit(FLOW_CHANGED);
}
addChangeListener(f) {
this.addListener(FLOW_CHANGED, f);
}
removeChangeListener(f) {
this.removeListener(FLOW_CHANGED, f);
}
}
class DummyFlowStore extends FlowStore {
constructor(flows) {
super();
this.flows = flows;
}
addFlow(f) {
this.flows.push(f);
this.emitChange();
}
}
SETTINGS_CHANGED = "settings.change";
class Settings extends EventEmitter {
constructor(){
super();
this.settings = false;
}
getAll(){
return this.settings;
}
emitChange() {
return this.emit(SETTINGS_CHANGED);
}
addChangeListener(f) {
this.addListener(SETTINGS_CHANGED, f);
}
removeChangeListener(f) {
this.removeListener(SETTINGS_CHANGED, f);
}
}
class DummySettings extends Settings {
constructor(settings){
super();
this.settings = settings;
}
update(obj){
_.merge(this.settings, obj);
this.emitChange();
}
}

View File

@ -0,0 +1,12 @@
/** @jsx React.DOM */
var Footer = React.createClass({
render : function(){
var style = {
textAlign: "center"
};
return (<footer>
<span className="label label-success">transparent mode</span>
</footer>);
}
});

View File

@ -0,0 +1,73 @@
/** @jsx React.DOM */
var MainMenu = React.createClass({
render : function(){
return (<div>Main Menu</div>);
}
});
var ToolsMenu = React.createClass({
render : function(){
return (<div>Tools Menu</div>);
}
});
var ReportsMenu = React.createClass({
render : function(){
return (<div>Reports Menu</div>);
}
});
var _Header_Entries = {
main: {
title: "Traffic",
route: "main",
menu: MainMenu
},
tools: {
title: "Tools",
route: "main",
menu: ToolsMenu
},
reports: {
title: "Visualization",
route: "reports",
menu: ReportsMenu
}
};
var Header = React.createClass({
getInitialState: function(){
return {active: "main"};
},
handleClick: function(active){
this.setState({active: active});
ReactRouter.transitionTo(_Header_Entries[active].route);
return false;
},
handleFileClick: function(){
console.log("File click");
},
render: function(){
var header = [];
for(var item in _Header_Entries){
var classes = this.state.active == item ? "active" : "";
header.push(<a key={item} href="#" className={classes}
onClick={this.handleClick.bind(this, item)}>{ _Header_Entries[item].title }</a>);
}
var menu = _Header_Entries[this.state.active].menu();
return (
<header>
<div className="title-bar">
mitmproxy { this.props.settings.version }
</div>
<nav>
<a href="#" className="special" onClick={this.handleFileClick}> File </a>
{header}
</nav>
<div className="menu">
{ menu }
</div>
</header>);
}
});

View File

@ -1,10 +0,0 @@
mitmproxy = function () {
function init() {
React.renderComponent(Router(), $("#mitmproxy")[0]);
}
var exports = {
init: init,
};
return exports;
}();

View File

@ -0,0 +1,60 @@
/** @jsx React.DOM */
var App = React.createClass({
getInitialState: function () {
return {
settings: {} //TODO: How explicit should we get here?
//List all subattributes?
};
},
componentDidMount: function () {
//TODO: Replace DummyStore with real settings over WS (https://facebook.github.io/react/tips/initial-ajax.html)
//TODO: Is there a sensible place where we can store this?
var settings = new DummySettings({
version: "0.12"
});
settings.addChangeListener(this._onSettingsChange);
//This would be async in some way or another.
this._onSettingsChange(null, settings);
},
_onSettingsChange: function(event, settings){
this.setState({settings: settings.getAll()});
},
render: function () {
return (
<div id="container">
<Header settings={this.state.settings}/>
<div id="main">
<this.props.activeRouteHandler settings={this.state.settings}/>
</div>
<Footer/>
</div>
);
}
});
var Traffic = React.createClass({
render: function(){
var json = JSON.stringify(this.props, null, 4);
var i = 5;
while(i--) json += json;
return (<pre>{json}</pre>);
}
});
var Reports = React.createClass({
render: function(){
return (<div>Report Editor</div>);
}
});
var routes = (
<ReactRouter.Routes location="hash">
<ReactRouter.Route name="app" path="/" handler={App}>
<ReactRouter.Route name="main" handler={Traffic}/>
<ReactRouter.Route name="reports" handler={Reports}/>
<ReactRouter.Redirect to="main"/>
</ReactRouter.Route>
</ReactRouter.Routes>
);

View File

@ -1,10 +0,0 @@
/** @jsx React.DOM */
var Router = React.createClass({
render: function(){
return <ReactRouter.Routes location="hash">
<ReactRouter.Route name="certs" path="/" handler={CertInstallView}/>
<ReactRouter.Route name="other" path="/other" handler={CertInstallView}/>
</ReactRouter.Routes>;
}
});

File diff suppressed because it is too large Load Diff