diff --git a/app/Container.react.js b/app/Container.react.js
index f69ca483b3..8369d64938 100644
--- a/app/Container.react.js
+++ b/app/Container.react.js
@@ -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
Hello
;
+ 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 ;
+ }
+
+ var logs = this.state.logs.map(function (l, i) {
+ return ;
+ });
+
+ var state;
+ if (container.State.Running) {
+ state = running
;
+ } else if (container.State.Restarting) {
+ state = restarting
;
+ }
+
+ return (
+
+
+
{container.Name.replace('/', '')}
{state}
+
+
+ {logs}
+
+
+ );
}
});
diff --git a/app/Containers.react.js b/app/Containers.react.js
index 4cd6f02d8f..e8623099c8 100644
--- a/app/Containers.react.js
+++ b/app/Containers.react.js
@@ -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 {container.Name.replace('/', '')}
+ var state;
+ if (container.State.Running) {
+ state = running;
+ } else if (container.State.Restarting) {
+ state = restarting;
+ }
+
+ return (
+
+
+
+ {container.Name.replace('/', '')}
+
+
+ {state} - {container.Config.Image}
+
+
+
+ );
});
return (
-
+
);
@@ -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 (
-
-
-
+
);
}
diff --git a/app/Header.react.js b/app/Header.react.js
new file mode 100644
index 0000000000..83fffe8ad9
--- /dev/null
+++ b/app/Header.react.js
@@ -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 (
+
+ );
+ }
+});
+
+module.exports = Header;
diff --git a/app/images/close.png b/app/images/close.png
new file mode 100644
index 0000000000..4551a82a0e
Binary files /dev/null and b/app/images/close.png differ
diff --git a/app/images/close@2x.png b/app/images/close@2x.png
new file mode 100644
index 0000000000..30793ab48f
Binary files /dev/null and b/app/images/close@2x.png differ
diff --git a/app/images/maximize.png b/app/images/maximize.png
new file mode 100644
index 0000000000..d0db77582e
Binary files /dev/null and b/app/images/maximize.png differ
diff --git a/app/images/maximize@2x.png b/app/images/maximize@2x.png
new file mode 100644
index 0000000000..447372fdb0
Binary files /dev/null and b/app/images/maximize@2x.png differ
diff --git a/app/images/minimize.png b/app/images/minimize.png
new file mode 100644
index 0000000000..731b123eb7
Binary files /dev/null and b/app/images/minimize.png differ
diff --git a/app/images/minimize@2x.png b/app/images/minimize@2x.png
new file mode 100644
index 0000000000..feb342d58c
Binary files /dev/null and b/app/images/minimize@2x.png differ
diff --git a/app/styles/clearsans.less b/app/styles/clearsans.less
new file mode 100644
index 0000000000..5456dc2740
--- /dev/null
+++ b/app/styles/clearsans.less
@@ -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;
+}
diff --git a/app/styles/main.less b/app/styles/main.less
index a11155248a..3d0d99884a 100644
--- a/app/styles/main.less
+++ b/app/styles/main.less
@@ -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;
}
diff --git a/app/styles/retina.less b/app/styles/retina.less
new file mode 100644
index 0000000000..02486e23bc
--- /dev/null
+++ b/app/styles/retina.less
@@ -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;
+ }
+}
diff --git a/browser/main.js b/browser/main.js
index 95f56946a2..e6fa0d7474 100644
--- a/browser/main.js
+++ b/browser/main.js
@@ -28,18 +28,17 @@ 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();
if (argv.test) {
mainWindow.loadUrl('file://' + __dirname + '/../build/specs.html');
- } else{
+ } else {
mainWindow.loadUrl('file://' + __dirname + '/../build/index.html');
}
@@ -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) {});
}
});
diff --git a/gulpfile.js b/gulpfile.js
index 39bab18c1e..dc57f568bb 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -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();
diff --git a/package.json b/package.json
index 8ffc7124e0..400ef40f23 100644
--- a/package.json
+++ b/package.json
@@ -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",