Merge branch 'dev'

Conflicts:
	dockerui.go
pull/2/head
Michael Crosby 2013-09-02 18:55:56 -07:00
commit 9eaa75a572
20 changed files with 823 additions and 88 deletions

View File

@ -6,10 +6,11 @@ MAINTAINER Michael Crosby http://crosbymichael.com
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
RUN apt-get update
RUN apt-get upgrade
RUN apt-get upgrade -y
ADD . /app/
RUN ln -s /app/dockerui /dockerui
EXPOSE 9000
WORKDIR /app/
ENTRYPOINT ["./dockerui"]

View File

@ -109,3 +109,10 @@
overflow-y: scroll;
overflow-x: hidden;
}
.legend .title {
margin: 0.5em;
border-style: solid;
border-width: 0 0 0 1em;
padding: 0 0.3em;
}

View File

@ -10,10 +10,10 @@ import (
)
var (
endpoint = flag.String("e", "", "Docker d endpoint.")
verbose = flag.Bool("v", false, "Verbose logging.")
port = flag.String("p", "9000", "Port to serve dockerui.")
assets = flag.String("-a", "/app", "Path to the assets.")
endpoint = flag.String("e", "", "Dockerd endpoint")
verbose = flag.Bool("v", false, "Verbose logging")
port = flag.String("p", "9000", "Port to serve dockerui")
assets = flag.String("a", ".", "Path to the assets")
)
type multiHandler struct {
@ -34,6 +34,7 @@ func (h *multiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func isDockerRequest(url string) bool {
log.Printf("Got request: %s", url)
return strings.Contains(url, "dockerapi/")
}

BIN
images/gritter-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
images/gritter-long.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
images/gritter.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
images/ie-spacer.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

View File

View File

@ -7,10 +7,11 @@
<meta name="description" content="">
<meta name="author" content="Michael Crosby crosbymichael.com">
<link href="../assets/css/bootstrap.css" rel="stylesheet">
<link href="../assets/css/bootstrap-responsive.css" rel="stylesheet">
<link href="assets/css/bootstrap.css" rel="stylesheet">
<link href="assets/css/bootstrap-responsive.css" rel="stylesheet">
<link href="lib/jquery.gritter.css" rel="stylesheet">
<link href="../css/app.css" rel="stylesheet">
<link href="css/app.css" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
@ -29,7 +30,6 @@
<div class="container">
<div ng-include="template" ng-controller="MastheadController"></div>
<div ng-include="template" ng-controller="MessageController"></div>
<div id="view" ng-view></div>
@ -51,12 +51,16 @@
<script src="../assets/js/bootstrap-carousel.js"></script>
<script src="../assets/js/bootstrap-typeahead.js"></script>
<script src="/lib/ace-builds/src-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="/lib/spin.js" type="text/javascript" charset="utf-8"></script>
<script src="lib/ace-builds/src-noconflict/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="lib/spin.js" type="text/javascript" charset="utf-8"></script>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular/angular-resource.js"></script>
<script src="lib/jquery.gritter.min.js"></script>
<script src="lib/Chart.min.js"></script>
<script src="lib/legend.js"></script>
<script src="js/app.js"></script>
<script src="js/services.js"></script>
<script src="js/filters.js"></script>

View File

@ -14,5 +14,5 @@ angular.module('dockerui', ['dockerui.services', 'dockerui.filters'])
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
.constant('DOCKER_ENDPOINT', '/dockerapi')
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243
.constant('UI_VERSION', 'v0.2')
.constant('DOCKER_API_VERSION', 'v1.2');
.constant('UI_VERSION', 'v0.3')
.constant('DOCKER_API_VERSION', 'v1.4');

View File

@ -3,22 +3,125 @@ function MastheadController($scope) {
$scope.template = 'partials/masthead.html';
}
function DashboardController($scope, Container) {
function newLineChart(id, data, getkey) {
var chart = getChart(id);
var map = {};
for (var i = 0; i < data.length; i++) {
var c = data[i];
var key = getkey(c);
var count = map[key];
if (count === undefined) {
count = 0;
}
count += 1;
map[key] = count;
}
var labels = [];
var data = [];
var keys = Object.keys(map);
for (var i = keys.length - 1; i > -1; i--) {
var k = keys[i];
labels.push(k);
data.push(map[k]);
}
var dataset = {
fillColor : "rgba(151,187,205,0.5)",
strokeColor : "rgba(151,187,205,1)",
pointColor : "rgba(151,187,205,1)",
pointStrokeColor : "#fff",
data : data
};
chart.Line({
labels: labels,
datasets: [dataset]
},
{
scaleStepWidth: 1,
pointDotRadius:1,
scaleOverride: true,
scaleSteps: labels.length
});
}
function MessageController($scope, Messages) {
$scope.template = 'partials/messages.html';
$scope.messages = [];
$scope.$watch('messages.length', function(o, n) {
$('#message-display').show();
});
function DashboardController($scope, Container, Image, Settings) {
$scope.predicate = '-Created';
$scope.containers = [];
$scope.$on(Messages.event, function(e, msg) {
$scope.messages.push(msg);
setTimeout(function() {
$('#message-display').hide('slow');
}, 30000);
});
var getStarted = function(data) {
$scope.totalContainers = data.length;
newLineChart('#containers-started-chart', data, function(c) { return new Date(c.Created * 1000).toLocaleDateString(); });
var s = $scope;
Image.query({}, function(d) {
s.totalImages = d.length;
newLineChart('#images-created-chart', d, function(c) { return new Date(c.Created * 1000).toLocaleDateString(); });
});
};
var opts = {animation:false};
if (Settings.firstLoad) {
$('#stats').hide();
opts.animation = true;
Settings.firstLoad = false;
$('#masthead').show();
setTimeout(function() {
$('#masthead').slideUp('slow');
$('#stats').slideDown('slow');
}, 5000);
}
Container.query({all: 1}, function(d) {
var running = 0
var ghost = 0;
var stopped = 0;
for (var i = 0; i < d.length; i++) {
var item = d[i];
if (item.Status === "Ghost") {
ghost += 1;
} else if (item.Status.indexOf('Exit') !== -1) {
stopped += 1;
} else {
running += 1;
$scope.containers.push(new ContainerViewModel(item));
}
}
getStarted(d);
var c = getChart('#containers-chart');
var data = [
{
value: running,
color: '#5bb75b',
title: 'Running'
}, // running
{
value: stopped,
color: '#C7604C',
title: 'Stopped'
}, // stopped
{
value: ghost,
color: '#E2EAE9',
title: 'Ghost'
} // ghost
];
c.Doughnut(data, opts);
var lgd = $('#chart-legend').get(0);
legend(lgd, data);
});
}
function getChart(id) {
var ctx = $(id).get(0).getContext("2d");
return new Chart(ctx);
}
function StatusBarController($scope, Settings) {
@ -53,46 +156,34 @@ function ContainerController($scope, $routeParams, $location, Container, Message
$scope.changes = [];
$scope.start = function(){
ViewSpinner.spin();
Container.start({id: $routeParams.id}, function(d) {
Messages.send({class: 'text-success', data: 'Container started.'});
ViewSpinner.stop();
Messages.send("Container started", $routeParams.id);
}, function(e) {
failedRequestHandler(e, Messages);
ViewSpinner.stop();
Messages.error("Failure", "Container failed to start." + e.data);
});
};
$scope.stop = function() {
ViewSpinner.spin();
Container.stop({id: $routeParams.id}, function(d) {
Messages.send({class: 'text-success', data: 'Container stopped.'});
ViewSpinner.stop();
Messages.send("Container stopped", $routeParams.id);
}, function(e) {
failedRequestHandler(e, Messages);
ViewSpinner.stop();
Messages.error("Failure", "Container failed to stop." + e.data);
});
};
$scope.kill = function() {
ViewSpinner.spin();
Container.kill({id: $routeParams.id}, function(d) {
Messages.send({class: 'text-success', data: 'Container killed.'});
ViewSpinner.stop();
Messages.send("Container killed", $routeParams.id);
}, function(e) {
failedRequestHandler(e, Messages);
ViewSpinner.stop();
Messages.error("Failure", "Container failed to die." + e.data);
});
};
$scope.remove = function() {
ViewSpinner.spin();
Container.remove({id: $routeParams.id}, function(d) {
Messages.send({class: 'text-success', data: 'Container removed.'});
ViewSpinner.stop();
Messages.send("Container removed", $routeParams.id);
}, function(e){
failedRequestHandler(e, Messages);
ViewSpinner.stop();
Messages.error("Failure", "Container failed to remove." + e.data);
});
};
@ -108,12 +199,14 @@ function ContainerController($scope, $routeParams, $location, Container, Message
Container.get({id: $routeParams.id}, function(d) {
$scope.container = d;
}, function(e) {
failedRequestHandler(e, Messages);
}, function(e) {
if (e.status === 404) {
$('.detail').hide();
Messages.error("Not found", "Container not found.");
} else {
Messages.error("Failure", e.data);
}
});
});
$scope.getChanges();
}
@ -132,7 +225,7 @@ function ContainersController($scope, Container, Settings, Messages, ViewSpinner
});
};
var batch = function(items, action) {
var batch = function(items, action, msg) {
ViewSpinner.spin();
var counter = 0;
var complete = function() {
@ -145,13 +238,13 @@ function ContainersController($scope, Container, Settings, Messages, ViewSpinner
if (c.Checked) {
counter = counter + 1;
action({id: c.Id}, function(d) {
Messages.send({class: 'text-success', data: 'Container ' + c.Id + ' Removed.'});
Messages.send("Container " + msg, c.Id);
var index = $scope.containers.indexOf(c);
$scope.containers.splice(index, 1);
complete();
}, function(e) {
failedRequestHandler(e, Messages);
complete();
Messages.error("Failure", e.data);
complete();
});
}
});
@ -174,19 +267,19 @@ function ContainersController($scope, Container, Settings, Messages, ViewSpinner
};
$scope.startAction = function() {
batch($scope.containers, Container.start);
batch($scope.containers, Container.start, "Started");
};
$scope.stopAction = function() {
batch($scope.containers, Container.stop);
batch($scope.containers, Container.stop, "Stopped");
};
$scope.killAction = function() {
batch($scope.containers, Container.kill);
batch($scope.containers, Container.kill, "Killed");
};
$scope.removeAction = function() {
batch($scope.containers, Container.remove);
batch($scope.containers, Container.remove, "Removed");
};
update({all: $scope.displayAll ? 1 : 0});
@ -215,14 +308,13 @@ function ImagesController($scope, Image, ViewSpinner, Messages) {
counter = counter + 1;
Image.remove({id: i.Id}, function(d) {
angular.forEach(d, function(resource) {
Messages.send({class: 'text-success', data: 'Deleted: ' + resource.Deleted});
Messages.send("Image deleted", resource.Deleted);
});
//Remove the image from the list
var index = $scope.images.indexOf(i);
$scope.images.splice(index, 1);
complete();
}, function(e) {
Messages.send({class: 'text-error', data: e.data});
Messages.error("Failure", e.data);
complete();
});
}
@ -240,21 +332,22 @@ function ImagesController($scope, Image, ViewSpinner, Messages) {
$scope.images = d.map(function(item) { return new ImageViewModel(item); });
ViewSpinner.stop();
}, function (e) {
failedRequestHandler(e, Messages);
Messages.error("Failure", e.data);
ViewSpinner.stop();
});
}
// Controller for a single image and actions on that image
function ImageController($scope, $routeParams, $location, Image, Messages) {
function ImageController($scope, $q, $routeParams, $location, Image, Container, Messages) {
$scope.history = [];
$scope.tag = {repo: '', force: false};
$scope.remove = function() {
Image.remove({id: $routeParams.id}, function(d) {
Messages.send({class: 'text-success', data: 'Image removed.'});
Messages.send("Image Removed", $routeParams.id);
}, function(e) {
failedRequestHandler(e, Messages);
$scope.error = e.data;
$('#error-message').show();
});
};
@ -267,9 +360,10 @@ function ImageController($scope, $routeParams, $location, Image, Messages) {
$scope.updateTag = function() {
var tag = $scope.tag;
Image.tag({id: $routeParams.id, repo: tag.repo, force: tag.force ? 1 : 0}, function(d) {
Messages.send({class: 'text-success', data: 'Tag added.'});
Messages.send("Tag Added", $routeParams.id);
}, function(e) {
failedRequestHandler(e, Messages);
$scope.error = e.data;
$('#error-message').show();
});
};
@ -279,11 +373,24 @@ function ImageController($scope, $routeParams, $location, Image, Messages) {
Image.get({id: $routeParams.id}, function(d) {
$scope.image = d;
$scope.tag = d.id;
var t = $routeParams.tag;
if (t && t !== ":") {
$scope.tag = t;
var promise = getContainersFromImage($q, Container, t);
promise.then(function(containers) {
newLineChart('#containers-started-chart', containers, function(c) { return new Date(c.Created * 1000).toLocaleDateString(); });
});
}
}, function(e) {
failedRequestHandler(e, Messages);
if (e.status === 404) {
$('.detail').hide();
$scope.error = "Image not found.<br />" + $routeParams.id;
} else {
$scope.error = e.data;
}
$('#error-message').show();
});
$scope.getHistory();
@ -355,3 +462,21 @@ function BuilderController($scope, Dockerfile, Messages) {
function failedRequestHandler(e, Messages) {
Messages.send({class: 'text-error', data: e.data});
}
// This gonna get messy but we don't have a good way to do this right now
function getContainersFromImage($q, Container, tag) {
var defer = $q.defer();
Container.query({all:1, notruc:1}, function(d) {
var containers = [];
for (var i = 0; i < d.length; i++) {
var c = d[i];
if (c.Image == tag) {
containers.push(new ContainerViewModel(c));
}
}
defer.resolve(containers);
});
return defer.promise;
}

View File

@ -64,7 +64,8 @@ angular.module('dockerui.services', ['ngResource'])
version: DOCKER_API_VERSION,
rawUrl: DOCKER_ENDPOINT + DOCKER_PORT + '/' + DOCKER_API_VERSION,
uiVersion: UI_VERSION,
url: url
url: url,
firstLoad: true,
};
})
.factory('ViewSpinner', function() {
@ -78,9 +79,30 @@ angular.module('dockerui.services', ['ngResource'])
})
.factory('Messages', function($rootScope) {
return {
event: 'messageSend',
send: function(msg) {
$rootScope.$broadcast('messageSend', msg);
send: function(title, text) {
$.gritter.add({
title: title,
text: text,
time: 2000,
before_open: function() {
if($('.gritter-item-wrapper').length == 3) {
return false;
}
}
});
},
error: function(title, text) {
$.gritter.add({
title: title,
text: text,
time: 6000,
before_open: function() {
if($('.gritter-item-wrapper').length == 4) {
return false;
}
}
});
}
};
})

101
lib/jquery.gritter.css Executable file
View File

@ -0,0 +1,101 @@
/* the norm */
#gritter-notice-wrapper {
position:fixed;
top:20px;
right:20px;
width:301px;
z-index:9999;
}
#gritter-notice-wrapper.top-left {
left: 20px;
right: auto;
}
#gritter-notice-wrapper.bottom-right {
top: auto;
left: auto;
bottom: 20px;
right: 20px;
}
#gritter-notice-wrapper.bottom-left {
top: auto;
right: auto;
bottom: 20px;
left: 20px;
}
.gritter-item-wrapper {
position:relative;
margin:0 0 10px 0;
background:url('../images/ie-spacer.gif'); /* ie7/8 fix */
}
.gritter-top {
background:url(../images/gritter.png) no-repeat left -30px;
height:10px;
}
.hover .gritter-top {
background-position:right -30px;
}
.gritter-bottom {
background:url(../images/gritter.png) no-repeat left bottom;
height:8px;
margin:0;
}
.hover .gritter-bottom {
background-position: bottom right;
}
.gritter-item {
display:block;
background:url(../images/gritter.png) no-repeat left -40px;
color:#eee;
padding:2px 11px 8px 11px;
font-size: 11px;
font-family:verdana;
}
.hover .gritter-item {
background-position:right -40px;
}
.gritter-item p {
padding:0;
margin:0;
word-wrap:break-word;
}
.gritter-close {
display:none;
position:absolute;
top:5px;
left:3px;
background:url(../images/gritter.png) no-repeat left top;
cursor:pointer;
width:30px;
height:30px;
}
.gritter-title {
font-size:14px;
font-weight:bold;
padding:0 0 7px 0;
display:block;
text-shadow:1px 1px 0 #000; /* Not supported by IE :( */
}
.gritter-image {
width:48px;
height:48px;
float:left;
}
.gritter-with-image,
.gritter-without-image {
padding:0;
}
.gritter-with-image {
width:220px;
float:right;
}
/* for the light (white) version of the gritter notice */
.gritter-light .gritter-item,
.gritter-light .gritter-bottom,
.gritter-light .gritter-top,
.gritter-light .gritter-close {
background-image: url(../images/gritter-light.png);
color: #222;
}
.gritter-light .gritter-title {
text-shadow: none;
}

418
lib/jquery.gritter.js Executable file
View File

@ -0,0 +1,418 @@
/*
* Gritter for jQuery
* http://www.boedesign.com/
*
* Copyright (c) 2012 Jordan Boesch
* Dual licensed under the MIT and GPL licenses.
*
* Date: February 24, 2012
* Version: 1.7.4
*/
(function($){
/**
* Set it up as an object under the jQuery namespace
*/
$.gritter = {};
/**
* Set up global options that the user can over-ride
*/
$.gritter.options = {
position: '',
class_name: '', // could be set to 'gritter-light' to use white notifications
fade_in_speed: 'medium', // how fast notifications fade in
fade_out_speed: 1000, // how fast the notices fade out
time: 6000 // hang on the screen for...
}
/**
* Add a gritter notification to the screen
* @see Gritter#add();
*/
$.gritter.add = function(params){
try {
return Gritter.add(params || {});
} catch(e) {
var err = 'Gritter Error: ' + e;
(typeof(console) != 'undefined' && console.error) ?
console.error(err, params) :
alert(err);
}
}
/**
* Remove a gritter notification from the screen
* @see Gritter#removeSpecific();
*/
$.gritter.remove = function(id, params){
Gritter.removeSpecific(id, params || {});
}
/**
* Remove all notifications
* @see Gritter#stop();
*/
$.gritter.removeAll = function(params){
Gritter.stop(params || {});
}
/**
* Big fat Gritter object
* @constructor (not really since its object literal)
*/
var Gritter = {
// Public - options to over-ride with $.gritter.options in "add"
position: '',
fade_in_speed: '',
fade_out_speed: '',
time: '',
// Private - no touchy the private parts
_custom_timer: 0,
_item_count: 0,
_is_setup: 0,
_tpl_close: '<div class="gritter-close"></div>',
_tpl_title: '<span class="gritter-title">[[title]]</span>',
_tpl_item: '<div id="gritter-item-[[number]]" class="gritter-item-wrapper [[item_class]]" style="display:none"><div class="gritter-top"></div><div class="gritter-item">[[close]][[image]]<div class="[[class_name]]">[[title]]<p>[[text]]</p></div><div style="clear:both"></div></div><div class="gritter-bottom"></div></div>',
_tpl_wrap: '<div id="gritter-notice-wrapper"></div>',
/**
* Add a gritter notification to the screen
* @param {Object} params The object that contains all the options for drawing the notification
* @return {Integer} The specific numeric id to that gritter notification
*/
add: function(params){
// Handle straight text
if(typeof(params) == 'string'){
params = {text:params};
}
// We might have some issues if we don't have a title or text!
if(params.text === null){
throw 'You must supply "text" parameter.';
}
// Check the options and set them once
if(!this._is_setup){
this._runSetup();
}
// Basics
var title = params.title,
text = params.text,
image = params.image || '',
sticky = params.sticky || false,
item_class = params.class_name || $.gritter.options.class_name,
position = $.gritter.options.position,
time_alive = params.time || '';
this._verifyWrapper();
this._item_count++;
var number = this._item_count,
tmp = this._tpl_item;
// Assign callbacks
$(['before_open', 'after_open', 'before_close', 'after_close']).each(function(i, val){
Gritter['_' + val + '_' + number] = ($.isFunction(params[val])) ? params[val] : function(){}
});
// Reset
this._custom_timer = 0;
// A custom fade time set
if(time_alive){
this._custom_timer = time_alive;
}
var image_str = (image != '') ? '<img src="' + image + '" class="gritter-image" />' : '',
class_name = (image != '') ? 'gritter-with-image' : 'gritter-without-image';
// String replacements on the template
if(title){
title = this._str_replace('[[title]]',title,this._tpl_title);
}else{
title = '';
}
tmp = this._str_replace(
['[[title]]', '[[text]]', '[[close]]', '[[image]]', '[[number]]', '[[class_name]]', '[[item_class]]'],
[title, text, this._tpl_close, image_str, this._item_count, class_name, item_class], tmp
);
// If it's false, don't show another gritter message
if(this['_before_open_' + number]() === false){
return false;
}
$('#gritter-notice-wrapper').addClass(position).append(tmp);
var item = $('#gritter-item-' + this._item_count);
item.fadeIn(this.fade_in_speed, function(){
Gritter['_after_open_' + number]($(this));
});
if(!sticky){
this._setFadeTimer(item, number);
}
// Bind the hover/unhover states
$(item).bind('mouseenter mouseleave', function(event){
if(event.type == 'mouseenter'){
if(!sticky){
Gritter._restoreItemIfFading($(this), number);
}
}
else {
if(!sticky){
Gritter._setFadeTimer($(this), number);
}
}
Gritter._hoverState($(this), event.type);
});
// Clicking (X) makes the perdy thing close
$(item).find('.gritter-close').click(function(){
Gritter.removeSpecific(number, {}, null, true);
});
return number;
},
/**
* If we don't have any more gritter notifications, get rid of the wrapper using this check
* @private
* @param {Integer} unique_id The ID of the element that was just deleted, use it for a callback
* @param {Object} e The jQuery element that we're going to perform the remove() action on
* @param {Boolean} manual_close Did we close the gritter dialog with the (X) button
*/
_countRemoveWrapper: function(unique_id, e, manual_close){
// Remove it then run the callback function
e.remove();
this['_after_close_' + unique_id](e, manual_close);
// Check if the wrapper is empty, if it is.. remove the wrapper
if($('.gritter-item-wrapper').length == 0){
$('#gritter-notice-wrapper').remove();
}
},
/**
* Fade out an element after it's been on the screen for x amount of time
* @private
* @param {Object} e The jQuery element to get rid of
* @param {Integer} unique_id The id of the element to remove
* @param {Object} params An optional list of params to set fade speeds etc.
* @param {Boolean} unbind_events Unbind the mouseenter/mouseleave events if they click (X)
*/
_fade: function(e, unique_id, params, unbind_events){
var params = params || {},
fade = (typeof(params.fade) != 'undefined') ? params.fade : true,
fade_out_speed = params.speed || this.fade_out_speed,
manual_close = unbind_events;
this['_before_close_' + unique_id](e, manual_close);
// If this is true, then we are coming from clicking the (X)
if(unbind_events){
e.unbind('mouseenter mouseleave');
}
// Fade it out or remove it
if(fade){
e.animate({
opacity: 0
}, fade_out_speed, function(){
e.animate({ height: 0 }, 300, function(){
Gritter._countRemoveWrapper(unique_id, e, manual_close);
})
})
}
else {
this._countRemoveWrapper(unique_id, e);
}
},
/**
* Perform actions based on the type of bind (mouseenter, mouseleave)
* @private
* @param {Object} e The jQuery element
* @param {String} type The type of action we're performing: mouseenter or mouseleave
*/
_hoverState: function(e, type){
// Change the border styles and add the (X) close button when you hover
if(type == 'mouseenter'){
e.addClass('hover');
// Show close button
e.find('.gritter-close').show();
}
// Remove the border styles and hide (X) close button when you mouse out
else {
e.removeClass('hover');
// Hide close button
e.find('.gritter-close').hide();
}
},
/**
* Remove a specific notification based on an ID
* @param {Integer} unique_id The ID used to delete a specific notification
* @param {Object} params A set of options passed in to determine how to get rid of it
* @param {Object} e The jQuery element that we're "fading" then removing
* @param {Boolean} unbind_events If we clicked on the (X) we set this to true to unbind mouseenter/mouseleave
*/
removeSpecific: function(unique_id, params, e, unbind_events){
if(!e){
var e = $('#gritter-item-' + unique_id);
}
// We set the fourth param to let the _fade function know to
// unbind the "mouseleave" event. Once you click (X) there's no going back!
this._fade(e, unique_id, params || {}, unbind_events);
},
/**
* If the item is fading out and we hover over it, restore it!
* @private
* @param {Object} e The HTML element to remove
* @param {Integer} unique_id The ID of the element
*/
_restoreItemIfFading: function(e, unique_id){
clearTimeout(this['_int_id_' + unique_id]);
e.stop().css({ opacity: '', height: '' });
},
/**
* Setup the global options - only once
* @private
*/
_runSetup: function(){
for(opt in $.gritter.options){
this[opt] = $.gritter.options[opt];
}
this._is_setup = 1;
},
/**
* Set the notification to fade out after a certain amount of time
* @private
* @param {Object} item The HTML element we're dealing with
* @param {Integer} unique_id The ID of the element
*/
_setFadeTimer: function(e, unique_id){
var timer_str = (this._custom_timer) ? this._custom_timer : this.time;
this['_int_id_' + unique_id] = setTimeout(function(){
Gritter._fade(e, unique_id);
}, timer_str);
},
/**
* Bring everything to a halt
* @param {Object} params A list of callback functions to pass when all notifications are removed
*/
stop: function(params){
// callbacks (if passed)
var before_close = ($.isFunction(params.before_close)) ? params.before_close : function(){};
var after_close = ($.isFunction(params.after_close)) ? params.after_close : function(){};
var wrap = $('#gritter-notice-wrapper');
before_close(wrap);
wrap.fadeOut(function(){
$(this).remove();
after_close();
});
},
/**
* An extremely handy PHP function ported to JS, works well for templating
* @private
* @param {String/Array} search A list of things to search for
* @param {String/Array} replace A list of things to replace the searches with
* @return {String} sa The output
*/
_str_replace: function(search, replace, subject, count){
var i = 0, j = 0, temp = '', repl = '', sl = 0, fl = 0,
f = [].concat(search),
r = [].concat(replace),
s = subject,
ra = r instanceof Array, sa = s instanceof Array;
s = [].concat(s);
if(count){
this.window[count] = 0;
}
for(i = 0, sl = s.length; i < sl; i++){
if(s[i] === ''){
continue;
}
for (j = 0, fl = f.length; j < fl; j++){
temp = s[i] + '';
repl = ra ? (r[j] !== undefined ? r[j] : '') : r[0];
s[i] = (temp).split(f[j]).join(repl);
if(count && s[i] !== temp){
this.window[count] += (temp.length-s[i].length) / f[j].length;
}
}
}
return sa ? s : s[0];
},
/**
* A check to make sure we have something to wrap our notices with
* @private
*/
_verifyWrapper: function(){
if($('#gritter-notice-wrapper').length == 0){
$('body').append(this._tpl_wrap);
}
}
}
})(jQuery);

1
lib/jquery.gritter.min.js vendored Normal file
View File

@ -0,0 +1 @@
(function(b){b.gritter={};b.gritter.options={position:"",class_name:"",fade_in_speed:"medium",fade_out_speed:1000,time:6000};b.gritter.add=function(f){try{return a.add(f||{})}catch(d){var c="Gritter Error: "+d;(typeof(console)!="undefined"&&console.error)?console.error(c,f):alert(c)}};b.gritter.remove=function(d,c){a.removeSpecific(d,c||{})};b.gritter.removeAll=function(c){a.stop(c||{})};var a={position:"",fade_in_speed:"",fade_out_speed:"",time:"",_custom_timer:0,_item_count:0,_is_setup:0,_tpl_close:'<div class="gritter-close"></div>',_tpl_title:'<span class="gritter-title">[[title]]</span>',_tpl_item:'<div id="gritter-item-[[number]]" class="gritter-item-wrapper [[item_class]]" style="display:none"><div class="gritter-top"></div><div class="gritter-item">[[close]][[image]]<div class="[[class_name]]">[[title]]<p>[[text]]</p></div><div style="clear:both"></div></div><div class="gritter-bottom"></div></div>',_tpl_wrap:'<div id="gritter-notice-wrapper"></div>',add:function(g){if(typeof(g)=="string"){g={text:g}}if(g.text===null){throw'You must supply "text" parameter.'}if(!this._is_setup){this._runSetup()}var k=g.title,n=g.text,e=g.image||"",l=g.sticky||false,m=g.class_name||b.gritter.options.class_name,j=b.gritter.options.position,d=g.time||"";this._verifyWrapper();this._item_count++;var f=this._item_count,i=this._tpl_item;b(["before_open","after_open","before_close","after_close"]).each(function(p,q){a["_"+q+"_"+f]=(b.isFunction(g[q]))?g[q]:function(){}});this._custom_timer=0;if(d){this._custom_timer=d}var c=(e!="")?'<img src="'+e+'" class="gritter-image" />':"",h=(e!="")?"gritter-with-image":"gritter-without-image";if(k){k=this._str_replace("[[title]]",k,this._tpl_title)}else{k=""}i=this._str_replace(["[[title]]","[[text]]","[[close]]","[[image]]","[[number]]","[[class_name]]","[[item_class]]"],[k,n,this._tpl_close,c,this._item_count,h,m],i);if(this["_before_open_"+f]()===false){return false}b("#gritter-notice-wrapper").addClass(j).append(i);var o=b("#gritter-item-"+this._item_count);o.fadeIn(this.fade_in_speed,function(){a["_after_open_"+f](b(this))});if(!l){this._setFadeTimer(o,f)}b(o).bind("mouseenter mouseleave",function(p){if(p.type=="mouseenter"){if(!l){a._restoreItemIfFading(b(this),f)}}else{if(!l){a._setFadeTimer(b(this),f)}}a._hoverState(b(this),p.type)});b(o).find(".gritter-close").click(function(){a.removeSpecific(f,{},null,true)});return f},_countRemoveWrapper:function(c,d,f){d.remove();this["_after_close_"+c](d,f);if(b(".gritter-item-wrapper").length==0){b("#gritter-notice-wrapper").remove()}},_fade:function(g,d,j,f){var j=j||{},i=(typeof(j.fade)!="undefined")?j.fade:true,c=j.speed||this.fade_out_speed,h=f;this["_before_close_"+d](g,h);if(f){g.unbind("mouseenter mouseleave")}if(i){g.animate({opacity:0},c,function(){g.animate({height:0},300,function(){a._countRemoveWrapper(d,g,h)})})}else{this._countRemoveWrapper(d,g)}},_hoverState:function(d,c){if(c=="mouseenter"){d.addClass("hover");d.find(".gritter-close").show()}else{d.removeClass("hover");d.find(".gritter-close").hide()}},removeSpecific:function(c,g,f,d){if(!f){var f=b("#gritter-item-"+c)}this._fade(f,c,g||{},d)},_restoreItemIfFading:function(d,c){clearTimeout(this["_int_id_"+c]);d.stop().css({opacity:"",height:""})},_runSetup:function(){for(opt in b.gritter.options){this[opt]=b.gritter.options[opt]}this._is_setup=1},_setFadeTimer:function(f,d){var c=(this._custom_timer)?this._custom_timer:this.time;this["_int_id_"+d]=setTimeout(function(){a._fade(f,d)},c)},stop:function(e){var c=(b.isFunction(e.before_close))?e.before_close:function(){};var f=(b.isFunction(e.after_close))?e.after_close:function(){};var d=b("#gritter-notice-wrapper");c(d);d.fadeOut(function(){b(this).remove();f()})},_str_replace:function(v,e,o,n){var k=0,h=0,t="",m="",g=0,q=0,l=[].concat(v),c=[].concat(e),u=o,d=c instanceof Array,p=u instanceof Array;u=[].concat(u);if(n){this.window[n]=0}for(k=0,g=u.length;k<g;k++){if(u[k]===""){continue}for(h=0,q=l.length;h<q;h++){t=u[k]+"";m=d?(c[h]!==undefined?c[h]:""):c[0];u[k]=(t).split(l[h]).join(m);if(n&&u[k]!==t){this.window[n]+=(t.length-u[k].length)/l[h].length}}}return p?u:u[0]},_verifyWrapper:function(){if(b("#gritter-notice-wrapper").length==0){b("body").append(this._tpl_wrap)}}}})(jQuery);

View File

@ -22,6 +22,27 @@
<td>Args:</td>
<td>{{ container.Args }}</td>
</tr>
<tr>
<td>Ports:</td>
<td>{{ container.Config.PortSpecs }}</td>
</tr>
<tr>
<td>Hostname:</td>
<td>{{ container.Config.Hostname }}</td>
</tr>
<tr>
<td>Cmd:</td>
<td>{{ container.Config.Cmd }}</td>
</tr>
<tr>
<td>Entrypoint:</td>
<td>{{ container.Config.Entrypoint }}</td>
</tr>
<tr>
<td>Volumes:</td>
<td>{{ container.Volumes }}</td>
</tr>
<tr>
<td>SysInitpath:</td>
<td>{{ container.SysInitPath }}</td>

View File

@ -1,13 +1,48 @@
<div class="row-fluid">
<div class="row-fluid center">
<!--<div class="sidebar span4">
<div ng-include="template" ng-controller="SideBarController"></div>
</div>-->
<div class="span12">
<div class="span12" id="masthead" style="display:none">
<div class="jumbotron">
<h1>DockerUI</h1>
<p class="lead">The Linux container engine</p>
<a class="btn btn-large btn-success" href="http://docker.io">Learn more.</a>
</div>
</div>
<div class="span12">
<div class="span6">
<h3>Running Containers</h3>
<ul>
<li ng-repeat="container in containers|orderBy:predicate">
<a href="/#/containers/{{ container.Id }}/">{{ container.Image }}</a>
<span class="label label-{{ container.Status|statusbadge }}">{{ container.Status }}</span>
</li>
</ul>
</div>
<div class="span6 pull-right">
<h3 style="float:right">Status</h3>
<canvas id="containers-chart" style="float:right;">
Get a better broswer... Your holding everyone back.
</canvas>
<div id="chart-legend" style="float:right;"></div>
</div>
</div>
<div class="span12" id="stats">
<hr />
<h4>Containers created</h4>
<canvas id="containers-started-chart" width="700" style="float:left">
Get a better broswer... Your holding everyone back.
</canvas>
<h1 style="float:right">{{ totalContainers }}</h1>
<hr />
<h4>Images created</h4>
<canvas id="images-created-chart" width="700" style="float:left">
Get a better broswer... Your holding everyone back.
</canvas>
<h1 style="float:right">{{ totalImages }}</h1>
</div>
</div>

View File

@ -1,13 +1,23 @@
<div ng-include="template" ng-controller="StartContainerController"></div>
<div class="alert alert-error" id="error-message" style="display:none">
{{ error }}
</div>
<div class="detail">
<h4>Image: {{ image.id }}</h4>
<h4>Image: {{ tag }}</h4>
<div class="btn-group detail">
<button class="btn btn-success" ng-click="create()">Create</button>
</div>
<div>
<h4>Containers created:</h4>
<canvas id="containers-started-chart" width="750">
Get a better broswer... Your holding everyone back.
</canvas>
</div>
<table class="table table-striped">
<tbody>
@ -19,10 +29,6 @@
<td>Parent:</td>
<td><a href="/#/images/{{ image.parent }}/">{{ image.parent }}</a></td>
</tr>
<tr>
<td>Container:</td>
<td><a href="/#/containers/{{ image.container }}/">{{ image.container }}</a></td>
</tr>
<tr>
<td>Size:</td>
<td>{{ image.Size|humansize }}</td>
@ -91,7 +97,6 @@
<hr />
<div class="btn-remove">
<button class="btn btn-large btn-block btn-primary btn-danger" ng-click="remove()">Remove Image</button>
</div>

View File

@ -4,7 +4,6 @@
<h2>Images:</h2>
<ul class="nav nav-pills">
<li class="active"><a href="" ng-click="showBuilder()">Build Image</a></li>
<li class="dropdown">
<a class="dropdown-toggle" id="drop4" role="button" data-toggle="dropdown" href="#">Actions <b class="caret"></b></a>
<ul id="menu1" class="dropdown-menu" role="menu" aria-labelledby="drop4">
@ -17,7 +16,6 @@
<tr>
<th><input type="checkbox" ng-model="toggle" ng-click="toggleSelectAll()" /> Action</th>
<th>Id</th>
<th>Tag</th>
<th>Repository</th>
<th>Created</th>
</tr>
@ -25,9 +23,8 @@
<tbody>
<tr ng-repeat="image in images | orderBy:predicate">
<td><input type="checkbox" ng-model="image.Checked" /></td>
<td><a href="/#/images/{{ image.Id }}/">{{ image.Id|truncate:20}}</a></td>
<td>{{ image.Tag }}</td>
<td>{{ image.Repository }}</td>
<td><a href="/#/images/{{ image.Id }}/?tag={{ image.Repository }}:{{ image.Tag }}">{{ image.Id|truncate:20}}</a></td>
<td>{{ image.Repository }}:{{ image.Tag }}</td>
<td>{{ image.Created|getdate }}</td>
</tr>
</tbody>

View File

@ -1,3 +0,0 @@
<div id="message-display" class="center well messages" style="display:none;">
<p ng-repeat="message in messages"><span class="{{ message.class }}">{{ message.data }}</span></p>
</div>