traffic lights

This commit is contained in:
Jeffrey Morgan 2015-01-13 16:45:18 -05:00
parent 4c3d3ad08b
commit 42417c3523
15 changed files with 434 additions and 22 deletions

View File

@ -1,3 +1,4 @@
var _ = require('underscore');
var React = require('react');
var Router = require('react-router');
var Route = Router.Route;
@ -6,9 +7,92 @@ var DefaultRoute = Router.DefaultRoute;
var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
var Convert = require('ansi-to-html');
var convert = new Convert();
var docker = require('./docker.js');
var Container = React.createClass({
mixins: [Router.State],
componentWillReceiveProps: function () {
var self = this;
var logs = [];
var index = 0;
docker.client().getContainer(this.getParams().Id).logs({
follow: false,
stdout: true,
timestamps: true
}, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (buf) {
// Every other message is a header
if (index % 2 === 1) {
var time = buf.substr(0,buf.indexOf(' '));
var msg = buf.substr(buf.indexOf(' ')+1);
logs.push(convert.toHtml(msg));
}
index += 1;
});
stream.on('end', function (buf) {
self.setState({logs: logs});
docker.client().getContainer(self.getParams().Id).logs({
follow: true,
stdout: true,
timestamps: true,
tail: 0
}, function (err, stream) {
stream.setEncoding('utf8');
stream.on('data', function (buf) {
// Every other message is a header
if (index % 2 === 1) {
var time = buf.substr(0,buf.indexOf(' '));
var msg = buf.substr(buf.indexOf(' ')+1);
logs.push(convert.toHtml(msg));
self.setState({logs: logs});
}
index += 1;
});
});
});
});
},
render: function () {
return <p>Hello</p>;
var self = this;
if (!this.state || !this.state.logs) {
return false;
}
var container = _.find(this.props.containers, function (container) {
return container.Id === self.getParams().Id;
});
// console.log(container);
if (!container || !this.state) {
return <div></div>;
}
var logs = this.state.logs.map(function (l, i) {
return <p key={i} dangerouslySetInnerHTML={{__html: l}}></p>;
});
var state;
if (container.State.Running) {
state = <h2 className="status">running</h2>;
} else if (container.State.Restarting) {
state = <h2 className="status">restarting</h2>;
}
return (
<div>
<div className="details-header">
<h1>{container.Name.replace('/', '')}</h1>{state}
</div>
<div className="logs">
{logs}
</div>
</div>
);
}
});

View File

@ -7,16 +7,37 @@ var Link = Router.Link;
var RouteHandler = Router.RouteHandler;
var Navigation= Router.Navigation;
var Header = require('./Header.react.js');
var async = require('async');
var docker = require('./docker.js');
var ContainerList = React.createClass({
render: function () {
var containers = this.props.containers.map(function (container) {
return <li key={container.Id}><Link to="container" params={container}>{container.Name.replace('/', '')}</Link></li>
var state;
if (container.State.Running) {
state = <span className="status">running</span>;
} else if (container.State.Restarting) {
state = <span className="status">restarting</span>;
}
return (
<Link key={container.Id} to="container" params={{Id: container.Id, container: container}}>
<li>
<div className="name">
{container.Name.replace('/', '')}
</div>
<div className="image">
{state} - {container.Config.Image}
</div>
</li>
</Link>
);
});
return (
<ul>
<ul className="container-list">
{containers}
</ul>
);
@ -26,7 +47,7 @@ var ContainerList = React.createClass({
var Containers = React.createClass({
mixins: [Navigation],
getInitialState: function() {
return {containers: []};
return {containers: [], index: null};
},
update: function () {
var self = this;
@ -37,7 +58,7 @@ var Containers = React.createClass({
});
}, function (err, results) {
if (results.length > 0) {
self.transitionTo('container', {Id: results[0].Id})
self.transitionTo('container', {Id: results[0].Id, container: results[0]});
}
self.setState({containers: results});
});
@ -58,9 +79,16 @@ var Containers = React.createClass({
},
render: function () {
return (
<div>
<div className="containers">
<Header/>
<div className="containers-body">
<div className="sidebar">
<ContainerList containers={this.state.containers}/>
<RouteHandler/>
</div>
<div className="details container">
<RouteHandler containers={this.state.containers}/>
</div>
</div>
</div>
);
}

27
app/Header.react.js Normal file
View File

@ -0,0 +1,27 @@
var React = require('react/addons');
var remote = require('remote');
var Header = React.createClass({
handleClose: function () {
remote.getCurrentWindow().hide();
},
handleMinimize: function () {
remote.getCurrentWindow().minimize();
},
handleMaximize: function () {
remote.getCurrentWindow().setFullScreen(!remote.getCurrentWindow().isFullScreen());
},
render: function () {
return (
<div className="header">
<div className="buttons">
<div className="button button-close" onClick={this.handleClose}></div>
<div className="button button-minimize" onClick={this.handleMinimize}></div>
<div className="button button-maximize" onClick={this.handleMaximize}></div>
</div>
</div>
);
}
});
module.exports = Header;

BIN
app/images/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

BIN
app/images/close@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 B

BIN
app/images/maximize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

BIN
app/images/maximize@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

BIN
app/images/minimize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 B

BIN
app/images/minimize@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

56
app/styles/clearsans.less Normal file
View File

@ -0,0 +1,56 @@
@font-face {
font-family: 'Clear';
src: url('clearsans-regular-webfont.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Clear Sans';
src: url('clearsans-medium-webfont.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Clear Sans';
src: url('clearsans-light-webfont.ttf') format('truetype');
font-weight: 300;
font-style: normal;
}
@font-face {
font-family: 'Clear Sans';
src: url('clearsans-thin-webfont.ttf') format('truetype');
font-weight: 100;
font-style: normal;
}
@font-face {
font-family: 'Clear Sans';
src: url('clearsans-mediumitalic-webfont.ttf') format('truetype');
font-weight: 500;
font-style: italic;
}
@font-face {
font-family: 'Clear Sans';
src: url('clearsans-italic-webfont.ttf') format('truetype');
font-weight: normal;
font-style: italic;
}
@font-face {
font-family: 'Clear Sans';
src: url('clearsans-bolditalic-webfont.ttf') format('truetype');
font-weight: 700;
font-style: italic;
}
@font-face {
font-family: 'Clear Sans';
src: url('clearsans-bold-webfont.ttf') format('truetype');
font-weight: 700;
font-style: normal;
}

View File

@ -1,7 +1,179 @@
@import "bootstrap/bootstrap.less";
@import "clearsans.less";
@import "retina.less";
@import "setup.less";
@import "radial.less";
body {
background: white;
.header {
height: 48px;
border-bottom: 1px solid #eee;
-webkit-app-region: drag;
-webkit-user-select: none;
.buttons {
display: inline-block;
position: relative;
top: 16px;
left: 20px;
&:hover {
.button-minimize {
.at2x('minimize.png', 10px, 10px);
}
.button-close {
.at2x('close.png', 10px, 10px);
}
.button-maximize {
.at2x('maximize.png', 10px, 10px);
}
}
.button {
box-sizing: border-box;
display: inline-block;
background: white;
margin-right: 9px;
height: 12px;
width: 12px;
border: 1px solid #CACDD0;
border-radius: 6px;
box-shadow: 0px 1px 1px 0px rgba(234,234,234,0.50);
-webkit-app-region: no-drag;
&:hover {
box-shadow: 0px 1px 1px 0px rgba(195,198,201,0.50);
}
&:active {
-webkit-filter: brightness(92%);
}
}
}
}
.container-list {
list-style: none;
padding: 0;
margin: 0;
overflow: auto;
min-width: 256px;
a {
color: inherit;
&:hover {
text-decoration: none;
}
&:focus {
text-decoration: none;
}
}
li {
&:hover {
background: #efefef;
}
padding: 14px 24px;
border-bottom: 1px solid #eee;
.name {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-size: 15px;
font-weight: 400;
color: #444;
}
.image {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
margin-top: 2px;
font-size: 13px;
color: #888;
}
}
}
.containers {
overflow: hidden;
height: 100%;
.containers-body {
display: flex;
height: 100%;
.sidebar {
height: 100%;
overflow: hidden;
min-width: 256px;
// border-right: 1px solid #eee;
margin: 0;
box-sizing: border-box;
display: flex;
.status {
font-size: 12px;
font-variant: small-caps;
color: @brand-primary;
}
}
.details {
width: auto;
margin: 0;
padding: 0;
position: relative;
box-sizing: border-box;
display: flex;
height: 100%;
overflow: hidden;
.details-header {
padding: 14px 45px;
background: white;
width: 100%;
h1 {
display: inline-block;
font-size: 24px;
}
h2 {
margin-left: 18px;
font-size: 14px;
display: inline-block;
font-variant: small-caps;
&.status {
color: @brand-success;
}
&.image {
}
}
}
.logs {
overflow: auto;
font-family: Menlo;
font-size: 12px;
padding: 44px 45px;
p {
margin: 6px;
}
}
}
}
}
html, body {
height: 100%;
width: 100%;
overflow: hidden;
-webkit-font-smoothing: subpixel-antialiased;
font-family: 'Helvetica Neue', sans-serif;
}

36
app/styles/retina.less Normal file
View File

@ -0,0 +1,36 @@
/*
The MIT License (MIT)
Copyright (c) 2013 Imulus, LLC, Ben Atkin, and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// retina.less
// A helper mixin for applying high-resolution background images (http://www.retinajs.com)
@highdpi: ~"(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-resolution: 1.5dppx)";
.at2x(@path, @w: auto, @h: auto) {
background-image: url(@path);
@at2x_path: ~`@{path}.replace(/\.\w+$/, function(match) { return "@2x" + match; })`;
@media @highdpi {
background-image: url("@{at2x_path}");
background-size: @w @h;
}
}

View File

@ -28,11 +28,10 @@ app.on('ready', function() {
var windowOptions = {
width: 1200,
height: 800,
'min-width': 1080,
'min-height': 560,
resizable: true,
frame: true,
'web-preferences': {
'web-security': false
}
frame: false
};
mainWindow = new BrowserWindow(windowOptions);
mainWindow.hide();
@ -48,7 +47,7 @@ app.on('ready', function() {
var saveVMOnQuit = true;
app.on('will-quit', function (e) {
if (saveVMOnQuit) {
exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {});
// exec('VBoxManage controlvm boot2docker-vm savestate', function (stderr, stdout, code) {});
}
});

View File

@ -114,9 +114,13 @@ gulp.task('images', function() {
gulp.task('styles', function () {
return gulp.src('app/styles/main.less')
.pipe(plumber())
.pipe(plumber(function(error) {
gutil.log(gutil.colors.red('Error (' + error.plugin + '): ' + error.message));
// emit the end event, to properly end the task
this.emit('end');
}))
.pipe(gulpif(options.dev, sourcemaps.init()))
.pipe(less()).on('error', console.error.bind(console))
.pipe(less())
.pipe(gulpif(options.dev, sourcemaps.write()))
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(!options.dev, cssmin()))
@ -135,6 +139,10 @@ gulp.task('copy', function () {
gulp.src('./app/index.html')
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev && !options.test, livereload()));
gulp.src('./app/fonts/**')
.pipe(gulp.dest(options.dev ? './build' : './dist/osx/' + options.filename + '/Contents/Resources/app/build'))
.pipe(gulpif(options.dev && !options.test, livereload()));
});
gulp.task('dist', function (cb) {
@ -206,8 +214,8 @@ gulp.task('test', ['download', 'copy', 'js', 'images', 'styles', 'specs'], funct
gulp.task('default', ['download', 'copy', 'js', 'images', 'styles'], function () {
gulp.watch('./app/**/*.html', ['copy']);
gulp.watch('./app/images/**', ['images']);
gulp.watch('./app/styles/**', ['styles']);
gulp.watch('./app/styles/**/*.less', ['styles']);
gulp.watch('./app/images/**/*.png', ['images']);
livereload.listen();

View File

@ -27,6 +27,8 @@
"dockerode": "2.0.4",
"exec": "0.1.2",
"flux-react": "^2.6.1",
"ftscroller": "^0.5.1",
"iscroll": "^5.1.3",
"leveldown": "^1.0.0",
"levelup": "git+https://github.com/kitematic/node-levelup.git",
"minimist": "^1.1.0",
@ -37,8 +39,8 @@
"react": "^0.12.1",
"request": "2.42.0",
"request-progress": "0.3.1",
"tar": "0.1.20",
"retina.js": "^1.1.0"
"retina.js": "^1.1.0",
"tar": "0.1.20"
},
"devDependencies": {
"browserify": "^6.2.0",