Major upgrade: Docker Manager is now a Standalone Ember App, contains

many improvements.
This commit is contained in:
Robin Ward 2014-05-02 15:38:09 -04:00
parent bfd70506bf
commit 3c804ec605
105 changed files with 63730 additions and 123 deletions

View File

@ -3,3 +3,13 @@ This is plugin works with the Discourse docker image.
It allows you to perform upgrades via the web UI and monitor activity in the container.
Warning: experimental.
### Development Notes
The client application is built using [Ember App Kit](https://github.com/stefanpenner/ember-app-kit).
In development mode, using `grunt server` will proxy to your Discourse instance running on Port 3000.
Just open up a browser to post 8000 and you're off to the races!.
To create a compiled version for distrubtion, run the `./compile_client.sh` to compile the site and
move it into the proper directories.

View File

@ -1,45 +0,0 @@
$(function(){
Discourse.MessageBus.start();
Discourse.MessageBus.subscribe("/docker/log", function(message){
if(message === "DONE"){
$("button.upgrade").attr("disabled", false);
} else {
$("#log").append($("<pre>" + message + "<pre>"));
}
});
$("button.upgrade").click(function(){
$("button.upgrade").attr("disabled", true);
Discourse.ajax({
url: "/admin/docker/upgrade",
data: { path: $(this).data("path") },
dataType: "text",
method: "POST"
}).then(function() {
alert("scroll to the bottom of your browser to watch the update");
});
});
var ps = function(){
Discourse.ajax({
url: "/admin/docker/ps",
dataType: "text"
}).then(
function(data){
$('#ps').text(data);
}
);
};
ps();
setInterval(ps, 5000);
Discourse.csrfToken = $('meta[name=csrf-token]').attr('content');
$.ajaxPrefilter(function(options, originalOptions, xhr) {
if (!options.crossDomain) {
xhr.setRequestHeader('X-CSRF-Token', Discourse.csrfToken);
}
});
});

View File

@ -1,22 +1,72 @@
require_dependency 'docker_manager/git_repo'
require_dependency 'docker_manager/upgrader'
module DockerManager
class AdminController < DockerManager::ApplicationController
layout nil
def index
require_dependency 'docker_manager/git_repo'
@main_repo = DockerManager::GitRepo.new(Rails.root)
render
end
def repos
repos = [DockerManager::GitRepo.new(Rails.root.to_s, 'discourse')]
Discourse.plugins.each do |p|
repos << DockerManager::GitRepo.new(File.dirname(p.path), p.name)
end
repos.map! do |r|
result = {name: r.name, path: r.path }
if r.valid?
result[:id] = r.name.downcase.gsub(/[^a-z]/, '_').gsub(/_+/, '_').sub(/_$/, '')
result[:version] = r.latest_local_commit
result[:url] = r.url
result[:upgrading] = r.upgrading?
end
result
end
render json: {repos: repos}
end
def progress
repo = DockerManager::GitRepo.new(params[:path])
upgrader = Upgrader.new(current_user.id, repo, params[:version])
render json: {progress: {logs: upgrader.find_logs, percentage: upgrader.last_percentage } }
end
def latest
repo = DockerManager::GitRepo.new(params[:path])
repo.update!
render json: {latest: {version: repo.latest_origin_commit,
commits_behind: repo.commits_behind,
date: repo.latest_origin_commit_date } }
end
def upgrade
require_dependency 'docker_manager/upgrader'
repo = DockerManager::GitRepo.new(params[:path])
Thread.new do
Upgrader.upgrade(current_user.id, params[:path])
upgrader = Upgrader.new(current_user.id, repo, params[:version])
upgrader.upgrade
end
render text: "OK"
end
def reset_upgrade
repo = DockerManager::GitRepo.new(params[:path])
upgrader = Upgrader.new(current_user.id, repo, params[:version])
upgrader.reset!
render text: "OK"
end
def ps
render text: `ps aux --sort -rss`
# Normally we don't run on OSX but this is useful for debugging
if RUBY_PLATFORM =~ /darwin/
ps_output = `ps aux -m`
else
ps_output = `ps aux --sort -rss`
end
render text: ps_output
end
def runaway_cpu

View File

@ -1,10 +0,0 @@
<% if repo.valid? %>
Current version: <%= repo.latest_local_commit %> (<%= time_ago_in_words repo.latest_local_commit_date %> ago),
Remote version: <a href="<%= repo.url %>"><%= repo.latest_origin_commit %></a> (<%= time_ago_in_words repo.latest_origin_commit_date %> ago)
<% if repo.commits_behind > 0 %>
commits behind: <%= repo.commits_behind %>
<button class="upgrade" action="" data-path="<%= repo.path %>"><%= upgrade_button_text %></button>
<% end %>
<% else %>
Not under source control.
<% end %>

View File

@ -1,45 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<%= csrf_meta_tags %>
<style>
#ps, #log { width: 1000px; max-height: 800px; height: 800px; overflow: auto; }
</style>
</head>
<body>
<h2>Discourse</h2>
<p>
<span style="color:red">You <i>must</i> manually refresh this page several times to get the latest version status. Yes, seriously.</span>
</p>
<p>
<%= render partial: 'git_status', locals: { repo: @main_repo, upgrade_button_text: "Upgrade Discourse" } %>
</p>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Docker Manager</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<h2>Plugins</h2>
<ul>
<% Discourse.plugins.each do |plugin| %>
<li>
<%= plugin.name %> - <%= render partial: 'git_status', locals: { repo: DockerManager::GitRepo.new(File.dirname(plugin.path)), upgrade_button_text: "Upgrade Plugin" } %>
</li>
<% end %>
</ul>
<%= javascript_include_tag "docker-manager-vendor" %>
<%= javascript_include_tag "docker-manager-app" %>
<%= javascript_include_tag "docker-manager-config" %>
<%= stylesheet_link_tag "docker-manager-app" %>
</head>
<body>
<script>
window.App = require('docker-manager/app')['default'].create(ENV.APP);
</script>
<h2>Processes</h2>
<pre id="ps"></pre>
<h2>Log</h2>
<div id="log"></div>
<div id="main"></div>
<script>
Discourse = {};
</script>
<%= javascript_include_tag "jquery_include.js" %>
<%= javascript_include_tag "message-bus.js" %>
<script>
Discourse.MessageBus = window.MessageBus;
Discourse.ajax = $.ajax;
Discourse.MessageBus.start();
</script>
<%= javascript_include_tag "docker_manager" %>
</body>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,774 @@
define("docker-manager/app",
["ember/resolver","ember/load-initializers","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
var Resolver = __dependency1__["default"];
var loadInitializers = __dependency2__["default"];
var App = Ember.Application.extend({
modulePrefix: 'docker-manager', // TODO: loaded via config
Resolver: Resolver
});
loadInitializers(App, 'docker-manager');
__exports__["default"] = App;
});
define("docker-manager/components/progress-bar",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = Em.Component.extend({
classNameBindings: [':progress'],
barStyle: function() {
var percent = parseInt(this.get('percent'), 10);
if (percent > 0) {
if (percent > 100) { percent = 100; }
return 'width: ' + this.get('percent') + '%';
}
}.property('percent')
});
});
define("docker-manager/components/x-tab",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = Em.Component.extend({
tagName: 'li',
classNameBindings: ['active'],
active: function() {
return this.get('childViews').anyBy('active');
}.property('childViews.@each.active')
});
});
define("docker-manager/controllers/index",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = Em.ObjectController.extend({
upgrading: null
});
});
define("docker-manager/controllers/processes",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = Ember.ObjectController.extend({
autoRefresh: false,
init: function() {
this._super();
var self = this;
window.setInterval(function() {
self.performRefresh();
}, 5000);
},
performRefresh: function() {
if (this.get('autoRefresh')) {
this.get('model').refresh();
}
}
});
});
define("docker-manager/controllers/repo",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = Em.ObjectController.extend({
needs: ['index'],
upgradingRepo: Em.computed.alias('controllers.index.upgrading'),
managerRepo: Em.computed.alias('controllers.index.managerRepo'),
upgradeDisabled: function() {
var upgradingRepo = this.get('upgradingRepo');
if (Em.isNone(upgradingRepo)) {
var managerRepo = this.get('managerRepo');
if (!managerRepo) { return false; }
return (!managerRepo.get('upToDate')) && managerRepo !== this.get('model');
}
return true;
}.property('upgradingRepo', 'model', 'managerRepo', 'managerRepo.upToDate')
});
});
define("docker-manager/controllers/upgrade",
["exports"],
function(__exports__) {
"use strict";
/* global MessageBus, bootbox */
__exports__["default"] = Em.ObjectController.extend({
init: function() {
this._super();
this.reset();
},
complete: Em.computed.equal('status', 'complete'),
failed: Em.computed.equal('status', 'failed'),
messageReceived: function(msg) {
switch(msg.type) {
case "log":
this.set('output', this.get('output') + msg.value + "\n");
break;
case "percent":
this.set('percent', msg.value);
break;
case "status":
this.set('status', msg.value);
if (msg.value === 'complete' || msg.value === 'failed') {
this.set('upgrading', false);
}
if (msg.value === 'complete') {
this.set('version', this.get('latest.version'));
}
break;
}
},
upgradeButtonText: function() {
if (this.get('upgrading')) {
return "Upgrading...";
} else {
return "Start Upgrading";
}
}.property('upgrading'),
startBus: function() {
var self = this;
MessageBus.subscribe("/docker/upgrade", function(msg) {
self.messageReceived(msg);
});
},
stopBus: function() {
MessageBus.unsubscribe("/docker/upgrade");
},
reset: function() {
this.setProperties({ output: '', status: null, percent: 0 });
},
actions: {
start: function() {
this.reset();
var repo = this.get('model');
if (repo.get('upgrading')) { return; }
repo.startUpgrade();
},
resetUpgrade: function() {
var self = this;
bootbox.confirm("<p><b>WARNING:</b> You should only reset upgrades that have failed and are not running.</p> <p>This will NOT cancel currently running builds and should only be used as a last resort.</p>", function(cancel) {
if (cancel) {
var repo = self.get('model');
repo.resetUpgrade().then(function() {
self.reset();
});
}
});
}
},
});
});
define("docker-manager/helpers/fmt-commit",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = Em.Handlebars.makeBoundHelper(function(sha1, url) {
if (Em.isNone(url)) { return; }
return new Em.Handlebars.SafeString("(<a href='" + url + "'>" + sha1 + "</a>)");
});
});
define("docker-manager/initializers/csrf-token",
["ic-ajax","exports"],
function(__dependency1__, __exports__) {
"use strict";
var ajax = __dependency1__["default"];
__exports__["default"] = {
name: "findCsrfToken",
initialize: function(container, application) {
return ajax('/session/csrf').then(function(result) {
var token = result.csrf;
$.ajaxPrefilter(function(options, originalOptions, xhr) {
if (!options.crossDomain) {
xhr.setRequestHeader('X-CSRF-Token', token);
}
});
});
}
};
});
define("docker-manager/models/process-list",
["ic-ajax","exports"],
function(__dependency1__, __exports__) {
"use strict";
var ajax = __dependency1__["default"];
var ProcessList = Em.Object.extend({
init: function() {
this._super();
},
refresh: function() {
var self = this;
return ajax("/admin/docker/ps").then(function(result) {
self.set('output', result);
return self;
});
}
});
ProcessList.reopenClass({
find: function() {
var list = ProcessList.create();
return list.refresh();
}
});
__exports__["default"] = ProcessList;
});
define("docker-manager/models/repo",
["ic-ajax","exports"],
function(__dependency1__, __exports__) {
"use strict";
var ajax = __dependency1__["default"];
var loaded = [];
var Repo = Em.Object.extend({
upToDate: function() {
return this.get('version') === this.get('latest.version');
}.property('version', 'latest.version'),
shouldCheck: function() {
if (Em.isNone(this.get('version'))) { return false; }
if (this.get('checking')) { return false; }
// Only check once every minute
var lastCheckedAt = this.get('lastCheckedAt');
if (lastCheckedAt) {
var ago = new Date().getTime() - lastCheckedAt;
return ago > 60 * 1000;
}
return true;
}.property().volatile(),
repoAjax: function(url, args) {
args = args || {};
args.data = this.getProperties('path', 'version');
return ajax(url, args);
},
findLatest: function() {
var self = this;
return new Em.RSVP.Promise(function(resolve, reject) {
if (!self.get('shouldCheck')) { return resolve(); }
self.set('checking', true);
self.repoAjax('/admin/docker/latest').then(function(result) {
self.setProperties({
checking: false,
lastCheckedAt: new Date().getTime(),
latest: Em.Object.create(result.latest)
});
resolve();
});
});
},
findProgress: function() {
return this.repoAjax('/admin/docker/progress').then(function(result) {
return result.progress;
});
},
resetUpgrade: function() {
var self = this;
return this.repoAjax('/admin/docker/upgrade', { type: 'DELETE' }).then(function() {
self.set('upgrading', false);
});
},
startUpgrade: function() {
var self = this;
this.set('upgrading', true);
return this.repoAjax('/admin/docker/upgrade', { type: 'POST' }).catch(function() {
self.set('upgrading', false);
});
}
});
Repo.reopenClass({
findAll: function() {
return new Em.RSVP.Promise(function (resolve) {
if (loaded.length) { return resolve(loaded); }
ajax("/admin/docker/repos").then(function(result) {
loaded = result.repos.map(function(r) {
return Repo.create(r);
});
resolve(loaded);
});
});
},
findUpgrading: function() {
return this.findAll().then(function(result) {
return result.findBy('upgrading', true);
});
},
find: function(id) {
return this.findAll().then(function(result) {
return result.findBy('id', id);
});
},
});
__exports__["default"] = Repo;
});
define("docker-manager/router",
["exports"],
function(__exports__) {
"use strict";
var Router = Ember.Router.extend(); // ensure we don't share routes between all Router instances
Router.map(function() {
this.route("processes");
this.resource('upgrade', { path: '/upgrade/:id' });
});
__exports__["default"] = Router;
});
define("docker-manager/routes/index",
["docker-manager/models/repo","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Repo = __dependency1__["default"];
__exports__["default"] = Em.Route.extend({
model: function() {
return Repo.findAll();
},
setupController: function(controller, model) {
controller.setProperties({ model: model, upgrading: null });
model.forEach(function(repo) {
repo.findLatest();
if (repo.get('upgrading')) {
controller.set('upgrading', repo);
}
// Special case: Upgrade docker manager first
if (repo.get('id') === 'docker_manager') {
controller.set('managerRepo', repo);
}
});
},
actions: {
upgrade: function(repo) {
this.transitionTo('upgrade', repo);
}
}
});
});
define("docker-manager/routes/processes",
["docker-manager/models/process-list","exports"],
function(__dependency1__, __exports__) {
"use strict";
var ProcessList = __dependency1__["default"];
__exports__["default"] = Em.Route.extend({
model: function() {
return ProcessList.find();
}
});
});
define("docker-manager/routes/upgrade",
["docker-manager/models/repo","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Repo = __dependency1__["default"];
__exports__["default"] = Em.Route.extend({
model: function(params) {
return Repo.find(params.id);
},
afterModel: function(model, transition) {
var self = this;
return Repo.findUpgrading().then(function(u) {
if (u && u !== model) {
return Ember.RSVP.Promise.reject("wat");
}
return model.findLatest().then(function() {
return model.findProgress().then(function(progress) {
self.set("progress", progress);
});
});
});
},
setupController: function(controller, model) {
controller.reset();
controller.setProperties({
model: model,
output: this.get('progress.logs'),
percent: this.get('progress.percentage')
});
controller.startBus();
},
deactivate: function() {
this.controllerFor('upgrade').stopBus();
}
});
});
define("docker-manager/utils/ajax",
["exports"],
function(__exports__) {
"use strict";
/* global ic */
__exports__["default"] = function ajax(){
return ic.ajax.apply(null, arguments);
}
});
define("docker-manager/views/loading",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = Em.View.extend({
_showOnInsert: function() {
var self = this;
self.set('runner', Em.run.later(function() {
self.$('h3').show();
}, 200));
}.on('didInsertElement'),
_cancelFade: function() {
Em.run.cancel(this.get('runner'));
}.on('willDestroyElement')
});
});
define("docker-manager/views/processes",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = Em.View.extend({
_insertedIntoDOM: function() {
this.set('controller.autoRefresh', true);
}.on('didInsertElement'),
_removedFromDOM: function() {
this.set('controller.autoRefresh', false);
}.on('willDestroyElement')
});
});
//# sourceMappingURL=app.js.map
define('docker-manager/templates/application', ['exports'], function(__exports__){ __exports__['default'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
var buffer = '', stack1, helper, options, self=this, helperMissing=helpers.helperMissing;
function program1(depth0,data) {
data.buffer.push("<img src=\"/assets/images/docker-manager-ea64623b074c8ec2b0303bae846e21e6.png\" class=\"logo\">");
}
function program3(depth0,data) {
data.buffer.push("Docker Manager");
}
function program5(depth0,data) {
data.buffer.push("Home");
}
function program7(depth0,data) {
data.buffer.push("Processes");
}
data.buffer.push("<header class=\"container\">\n ");
stack1 = (helper = helpers['link-to'] || (depth0 && depth0['link-to']),options={hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["STRING"],data:data},helper ? helper.call(depth0, "index", options) : helperMissing.call(depth0, "link-to", "index", options));
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n <h1>");
stack1 = (helper = helpers['link-to'] || (depth0 && depth0['link-to']),options={hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["STRING"],data:data},helper ? helper.call(depth0, "index", options) : helperMissing.call(depth0, "link-to", "index", options));
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("</h1>\n</header>\n\n\n<div class=\"container\">\n\n <ul class=\"nav nav-tabs\">\n ");
stack1 = (helper = helpers['x-tab'] || (depth0 && depth0['x-tab']),options={hash:{
'route': ("index")
},hashTypes:{'route': "STRING"},hashContexts:{'route': depth0},inverse:self.noop,fn:self.program(5, program5, data),contexts:[],types:[],data:data},helper ? helper.call(depth0, options) : helperMissing.call(depth0, "x-tab", options));
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n ");
stack1 = (helper = helpers['x-tab'] || (depth0 && depth0['x-tab']),options={hash:{
'route': ("processes")
},hashTypes:{'route': "STRING"},hashContexts:{'route': depth0},inverse:self.noop,fn:self.program(7, program7, data),contexts:[],types:[],data:data},helper ? helper.call(depth0, options) : helperMissing.call(depth0, "x-tab", options));
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n </ul>\n\n ");
stack1 = helpers._triageMustache.call(depth0, "outlet", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n</div>\n");
return buffer;
}); });
define('docker-manager/templates/components/progress-bar', ['exports'], function(__exports__){ __exports__['default'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
var buffer = '', escapeExpression=this.escapeExpression;
data.buffer.push("<div class=\"progress-bar\" ");
data.buffer.push(escapeExpression(helpers['bind-attr'].call(depth0, {hash:{
'style': ("barStyle")
},hashTypes:{'style': "STRING"},hashContexts:{'style': depth0},contexts:[],types:[],data:data})));
data.buffer.push("></div>\n");
return buffer;
}); });
define('docker-manager/templates/components/x-tab', ['exports'], function(__exports__){ __exports__['default'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
var buffer = '', stack1, helper, options, self=this, helperMissing=helpers.helperMissing;
function program1(depth0,data) {
var stack1;
stack1 = helpers._triageMustache.call(depth0, "yield", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
else { data.buffer.push(''); }
}
stack1 = (helper = helpers['link-to'] || (depth0 && depth0['link-to']),options={hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data},helper ? helper.call(depth0, "route", options) : helperMissing.call(depth0, "link-to", "route", options));
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n");
return buffer;
}); });
define('docker-manager/templates/index', ['exports'], function(__exports__){ __exports__['default'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
var buffer = '', stack1, escapeExpression=this.escapeExpression, helperMissing=helpers.helperMissing, self=this;
function program1(depth0,data) {
var buffer = '', stack1, helper, options;
data.buffer.push("\n <tr>\n <td>\n ");
stack1 = helpers._triageMustache.call(depth0, "name", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n ");
data.buffer.push(escapeExpression((helper = helpers['fmt-commit'] || (depth0 && depth0['fmt-commit']),options={hash:{},hashTypes:{},hashContexts:{},contexts:[depth0,depth0],types:["ID","ID"],data:data},helper ? helper.call(depth0, "version", "url", options) : helperMissing.call(depth0, "fmt-commit", "version", "url", options))));
data.buffer.push("\n </td>\n <td>\n ");
stack1 = helpers['if'].call(depth0, "checking", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(4, program4, data),fn:self.program(2, program2, data),contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n </td>\n </tr>\n ");
return buffer;
}
function program2(depth0,data) {
data.buffer.push("\n Checking for new version...\n ");
}
function program4(depth0,data) {
var buffer = '', stack1;
data.buffer.push("\n ");
stack1 = helpers['if'].call(depth0, "upToDate", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(7, program7, data),fn:self.program(5, program5, data),contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n ");
return buffer;
}
function program5(depth0,data) {
data.buffer.push("\n Up to date\n ");
}
function program7(depth0,data) {
var buffer = '', stack1, helper, options;
data.buffer.push("\n <div class='new-version'>\n <h4>New Version Available!</h4>\n <ul>\n <li>Remote Version: ");
data.buffer.push(escapeExpression((helper = helpers['fmt-commit'] || (depth0 && depth0['fmt-commit']),options={hash:{},hashTypes:{},hashContexts:{},contexts:[depth0,depth0],types:["ID","ID"],data:data},helper ? helper.call(depth0, "latest.version", "url", options) : helperMissing.call(depth0, "fmt-commit", "latest.version", "url", options))));
data.buffer.push("</li>\n <li>Last Updated: ");
stack1 = helpers._triageMustache.call(depth0, "latest.date", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("</li>\n <li class='new-commits'>");
stack1 = helpers._triageMustache.call(depth0, "latest.commits_behind", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push(" new commits</li>\n </ul>\n ");
stack1 = helpers['if'].call(depth0, "upgrading", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(10, program10, data),fn:self.program(8, program8, data),contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n </div>\n ");
return buffer;
}
function program8(depth0,data) {
var buffer = '';
data.buffer.push("\n <button class=\"btn\" ");
data.buffer.push(escapeExpression(helpers.action.call(depth0, "upgrade", "", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0,depth0],types:["ID","ID"],data:data})));
data.buffer.push(">Currently Upgrading...</button>\n ");
return buffer;
}
function program10(depth0,data) {
var buffer = '';
data.buffer.push("\n <button class=\"btn\" ");
data.buffer.push(escapeExpression(helpers.action.call(depth0, "upgrade", "", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0,depth0],types:["ID","ID"],data:data})));
data.buffer.push(" ");
data.buffer.push(escapeExpression(helpers['bind-attr'].call(depth0, {hash:{
'disabled': ("upgradeDisabled")
},hashTypes:{'disabled': "STRING"},hashContexts:{'disabled': depth0},contexts:[],types:[],data:data})));
data.buffer.push(">Upgrade to the Latest Version</button>\n ");
return buffer;
}
data.buffer.push("<h3>Repositories</h3>\n\n<table class='table' id='repos'>\n <tr>\n <th style='width: 50%'>Name</th>\n <th>Status</th>\n </tr>\n <tbody>\n ");
stack1 = helpers.each.call(depth0, "model", {hash:{
'itemController': ("repo")
},hashTypes:{'itemController': "STRING"},hashContexts:{'itemController': depth0},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n </tbody>\n</table>\n");
return buffer;
}); });
define('docker-manager/templates/loading', ['exports'], function(__exports__){ __exports__['default'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
data.buffer.push("<h3 class='loading'>Loading...</h3>\n");
}); });
define('docker-manager/templates/processes', ['exports'], function(__exports__){ __exports__['default'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
var buffer = '', stack1;
data.buffer.push("<h3>Processes</h3>\n\n<div class='logs'>");
stack1 = helpers._triageMustache.call(depth0, "output", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("</div>\n");
return buffer;
}); });
define('docker-manager/templates/upgrade', ['exports'], function(__exports__){ __exports__['default'] = Ember.Handlebars.template(function anonymous(Handlebars,depth0,helpers,partials,data) {
this.compilerInfo = [4,'>= 1.0.0'];
helpers = this.merge(helpers, Ember.Handlebars.helpers); data = data || {};
var buffer = '', stack1, helper, options, helperMissing=helpers.helperMissing, escapeExpression=this.escapeExpression, self=this;
function program1(depth0,data) {
data.buffer.push("\n <p>Upgrade completed successfully!</p>\n <p>Note: The web server restarts in the background. It's a good idea to wait 30 seconds or so\n before refreshing your browser to see the latest version of the application.</p>\n");
}
function program3(depth0,data) {
data.buffer.push("\n <p>Sorry, there wasn an error upgrading Discourse. Please check the logs.</p>\n");
}
function program5(depth0,data) {
var buffer = '', stack1, helper, options;
data.buffer.push("\n <p>");
stack1 = helpers._triageMustache.call(depth0, "name", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push(" is at the newest version ");
data.buffer.push(escapeExpression((helper = helpers['fmt-commit'] || (depth0 && depth0['fmt-commit']),options={hash:{},hashTypes:{},hashContexts:{},contexts:[depth0,depth0],types:["ID","ID"],data:data},helper ? helper.call(depth0, "version", "url", options) : helperMissing.call(depth0, "fmt-commit", "version", "url", options))));
data.buffer.push(".</p>\n");
return buffer;
}
function program7(depth0,data) {
var buffer = '', stack1;
data.buffer.push("\n <div style='clear: both'>\n <button ");
data.buffer.push(escapeExpression(helpers.action.call(depth0, "start", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data})));
data.buffer.push(" ");
data.buffer.push(escapeExpression(helpers['bind-attr'].call(depth0, {hash:{
'disabled': ("upgrading")
},hashTypes:{'disabled': "STRING"},hashContexts:{'disabled': depth0},contexts:[],types:[],data:data})));
data.buffer.push(" class='btn'>");
stack1 = helpers._triageMustache.call(depth0, "upgradeButtonText", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("</button>\n ");
stack1 = helpers['if'].call(depth0, "upgrading", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(8, program8, data),contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n </div>\n");
return buffer;
}
function program8(depth0,data) {
var buffer = '';
data.buffer.push("\n <button ");
data.buffer.push(escapeExpression(helpers.action.call(depth0, "resetUpgrade", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data})));
data.buffer.push(" class=\"btn unlock\">Reset Upgrade</button>\n ");
return buffer;
}
data.buffer.push("<h3>Upgrade ");
stack1 = helpers._triageMustache.call(depth0, "name", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("</h3>\n\n");
data.buffer.push(escapeExpression((helper = helpers['progress-bar'] || (depth0 && depth0['progress-bar']),options={hash:{
'percent': ("percent")
},hashTypes:{'percent': "ID"},hashContexts:{'percent': depth0},contexts:[],types:[],data:data},helper ? helper.call(depth0, options) : helperMissing.call(depth0, "progress-bar", options))));
data.buffer.push("\n\n");
stack1 = helpers['if'].call(depth0, "complete", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(1, program1, data),contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n\n");
stack1 = helpers['if'].call(depth0, "failed", {hash:{},hashTypes:{},hashContexts:{},inverse:self.noop,fn:self.program(3, program3, data),contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n\n");
stack1 = helpers['if'].call(depth0, "upToDate", {hash:{},hashTypes:{},hashContexts:{},inverse:self.program(7, program7, data),fn:self.program(5, program5, data),contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("\n\n<div class='logs'>");
stack1 = helpers._triageMustache.call(depth0, "output", {hash:{},hashTypes:{},hashContexts:{},contexts:[depth0],types:["ID"],data:data});
if(stack1 || stack1 === 0) { data.buffer.push(stack1); }
data.buffer.push("</div>\n");
return buffer;
}); });

View File

@ -0,0 +1,18 @@
// Put general configuration here. This file is included
// in both production and development BEFORE Ember is
// loaded.
//
// For example to enable a feature on a canary build you
// might do:
//
// window.ENV = {FEATURES: {'with-controller': true}};
window.ENV = window.ENV || {};
window.ENV.MODEL_FACTORY_INJECTIONS = true;
// Put your production configuration here.
//
// This is useful when using a separate API
// endpoint in development than in production.
//
// window.ENV.public_key = '123456'

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

12
compile_client.sh Executable file
View File

@ -0,0 +1,12 @@
#!/bin/bash
# Compile the application using grunt
(cd manager-client && grunt dist)
# Remove old assets
rm assets/docker-manager-*
cp manager-client/dist/assets/app.min.css assets/docker-manager-app.css
cp manager-client/dist/assets/app.min.js assets/docker-manager-app.js
cp manager-client/dist/assets/vendor.min.js assets/docker-manager-vendor.js
cp manager-client/dist/assets/config.min.js assets/docker-manager-config.js

View File

@ -1,7 +1,12 @@
DockerManager::Engine.routes.draw do
get "admin/docker" => "admin#index"
get "admin/docker/repos" => "admin#repos"
get "admin/docker/latest" => "admin#latest"
get "admin/docker/progress" => "admin#progress"
get "admin/docker/ps" => "admin#ps"
post "admin/docker/upgrade" => "admin#upgrade"
delete "admin/docker/upgrade" => "admin#reset_upgrade"
get "admin/docker/runaway_cpu" => "admin#runaway_cpu"
get "admin/docker/runaway_mem" => "admin#runaway_mem"
get 'admin/docker/csrf' => 'admin#csrf'
end

View File

@ -1,12 +1,25 @@
# like Grit just very very minimal
class DockerManager::GitRepo
attr_reader :path
attr_reader :path, :name
def initialize(path)
def initialize(path, name=nil)
@path = path
@name = name
@memoize = {}
end
def start_upgrading
$redis.setnx(upgrade_key, 1)
end
def stop_upgrading
$redis.del(upgrade_key)
end
def upgrading?
$redis.get(upgrade_key).present?
end
def valid?
File.directory?("#{path}/.git")
end
@ -41,8 +54,16 @@ class DockerManager::GitRepo
url
end
def update!
`cd #{path} && git remote update`
end
protected
def upgrade_key
@upgrade_key ||= "upgrade:#{path}"
end
def commit_date(commit)
unix_timestamp = run('show -s --format="%ct" ' << commit).to_i
Time.at(unix_timestamp).to_datetime
@ -52,15 +73,7 @@ class DockerManager::GitRepo
run "for-each-ref --format='%(upstream:short)' $(git symbolic-ref -q HEAD)"
end
def ensure_updated
@updated ||= Thread.new do
# this is a very slow operation, make it async
`cd #{path} && git remote update`
end
end
def run(cmd)
ensure_updated
@memoize[cmd] ||= `cd #{path} && git #{cmd}`.strip
rescue => e
p e

View File

@ -1,25 +1,35 @@
class DockerManager::Upgrader
attr_accessor :user_id, :path
def self.upgrade(user_id, path)
self.new(user_id: user_id, path: path).upgrade
def initialize(user_id, repo, from_version)
@user_id = user_id
@repo = repo
@from_version = from_version
end
def initialize(opts)
self.user_id = opts[:user_id]
self.path= opts[:path]
def reset!
@repo.stop_upgrading
clear_logs
percent(0)
end
def upgrade
return unless @repo.start_upgrading
clear_logs
# HEAD@{upstream} is just a fancy way how to say origin/master (in normal case)
# see http://stackoverflow.com/a/12699604/84283
run("cd #{path} && git fetch && git reset --hard HEAD@{upstream}")
run("cd #{@repo.path} && git fetch && git reset --hard HEAD@{upstream}")
log("********************************************************")
log("*** Please be patient, next steps might take a while ***")
log("********************************************************")
run("bundle install --deployment --without test --without development")
percent(25)
run("bundle exec rake multisite:migrate")
percent(50)
log("*** Bundling assets. This might take a while *** ")
run("bundle exec rake assets:precompile")
percent(75)
sidekiq_pid = `ps aux | grep sidekiq.*busy | grep -v grep | awk '{ print $2 }'`.strip.to_i
if sidekiq_pid > 0
Process.kill("TERM", sidekiq_pid)
@ -27,6 +37,8 @@ class DockerManager::Upgrader
else
log("Warning: Sidekiq was not found")
end
percent(100)
publish('status', 'complete')
pid = `ps aux | grep unicorn_launcher | grep -v sudo | grep -v grep | awk '{ print $2 }'`.strip
if pid.to_i > 0
log("***********************************************")
@ -38,9 +50,17 @@ class DockerManager::Upgrader
else
log("Did not find unicorn launcher")
end
rescue
rescue => ex
publish('status', 'failed')
STDERR.puts("Docker Manager: FAILED TO UPGRADE")
STDERR.puts(ex.inspect)
raise
ensure
@repo.stop_upgrading
end
def publish(type, value)
MessageBus.publish("/docker/upgrade", {type: type, value: value}, user_ids: [@user_id])
end
def run(cmd)
@ -67,7 +87,35 @@ class DockerManager::Upgrader
end
end
def logs_key
"logs:#{@repo.path}:#{@from_version}"
end
def clear_logs
$redis.del(logs_key)
end
def find_logs
$redis.get(logs_key)
end
def percent_key
"percent:#{@repo.path}:#{@from_version}"
end
def last_percentage
$redis.get(percent_key)
end
def percent(val)
$redis.set(percent_key, val)
$redis.expire(percent_key, 30.minutes)
publish('percent', val)
end
def log(message)
MessageBus.publish("/docker/log", message, user_ids: [user_id])
$redis.append logs_key, message + "\n"
$redis.expire(logs_key, 30.minutes)
publish 'log', message
end
end

3
manager-client/.bowerrc Normal file
View File

@ -0,0 +1,3 @@
{
"directory": "vendor"
}

20
manager-client/.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/docs
# dependencies
/node_modules
/vendor/*
!/vendor/ember-shim.js
!/vendor/qunit-shim.js
# misc
/.sass-cache
/connect.lock
/libpeerconnection.log
.DS_Store
Thumbs.db
/coverage/*

37
manager-client/.jshintrc Normal file
View File

@ -0,0 +1,37 @@
{
"predef": {
"document": true,
"window": true,
"location": true,
"setTimeout": true,
"Ember": true,
"Em": true,
"DS": true,
"$": true
},
"node" : false,
"browser" : false,
"boss" : true,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esnext": true
}

View File

@ -0,0 +1,7 @@
language: node_js
node_js:
- 0.10
before_script:
- npm install -g grunt-cli
- npm install -g bower
- bower install

240
manager-client/Gruntfile.js Normal file
View File

@ -0,0 +1,240 @@
// jshint node:true
module.exports = function(grunt) {
// To support Coffeescript, SASS, LESS and others, just install
// the appropriate grunt package and it will be automatically included
// in the build process:
//
// * for Coffeescript, run `npm install --save-dev grunt-contrib-coffee`
//
// * for SCSS (without SASS), run `npm install --save-dev grunt-sass`
// * for SCSS/SASS support (may be slower), run
// `npm install --save-dev grunt-contrib-sass`
// This depends on the ruby sass gem, which can be installed with
// `gem install sass`
// * for Compass, run `npm install --save-dev grunt-contrib-compass`
// This depends on the ruby compass gem, which can be installed with
// `gem install compass`
// You should not install SASS if you have installed Compass.
//
// * for LESS, run `npm install --save-dev grunt-contrib-less`
//
// * for Stylus/Nib, `npm install --save-dev grunt-contrib-stylus`
//
// * for Emblem, run the following commands:
// `npm uninstall --save-dev grunt-ember-templates`
// `npm install --save-dev grunt-emblem`
// `bower install emblem.js --save`
//
// * For EmberScript, run `npm install --save-dev grunt-ember-script`
//
// * for LiveReload, `npm install --save-dev connect-livereload`
//
// * for YUIDoc support, `npm install --save-dev grunt-contrib-yuidoc`
// It is also nice to use a theme other than default. For example,
// simply do: `npm install yuidoc-theme-blue`
// Currently, only the `app` directory is used for generating docs.
// When installed, visit: http[s]://[host:port]/docs
//
// * for displaying the execution time of the grunt tasks,
// `npm install --save-dev time-grunt`
//
// * for minimizing the index.html at the end of the dist task
// `npm install --save-dev grunt-contrib-htmlmin`
//
// * for minimizing images in the dist task
// `npm install --save-dev grunt-contrib-imagemin`
//
// * for using images based CSS sprites (http://youtu.be/xD8DW6IQ6r0)
// `npm install --save-dev grunt-fancy-sprites`
// `bower install --save fancy-sprites-scss`
//
// * for automatically adding CSS vendor prefixes (autoprefixer)
// `npm install --save-dev grunt-autoprefixer`
//
// * for package import validations
// `npm install --save-dev grunt-es6-import-validate`
//
var Helpers = require('./tasks/helpers'),
filterAvailable = Helpers.filterAvailableTasks,
_ = grunt.util._,
path = require('path');
Helpers.pkg = require("./package.json");
if (Helpers.isPackageAvailable("time-grunt")) {
require("time-grunt")(grunt);
}
// Loads task options from `tasks/options/` and `tasks/custom-options`
// and loads tasks defined in `package.json`
var config = _.extend({},
require('load-grunt-config')(grunt, {
configPath: path.join(__dirname, 'tasks/options'),
loadGruntTasks: false,
init: false
}),
require('load-grunt-config')(grunt, { // Custom options have precedence
configPath: path.join(__dirname, 'tasks/custom-options'),
init: false
})
);
grunt.loadTasks('tasks'); // Loads tasks in `tasks/` folder
config.env = process.env;
// App Kit's Main Tasks
// ====================
// Generate the production version
// ------------------
grunt.registerTask('dist', "Build a minified & production-ready version of your app.", [
'clean:dist',
'build:dist',
'copy:assemble',
'createDistVersion'
]);
// Default Task
// ------------------
grunt.registerTask('default', "Build (in debug mode)");
// Servers
// -------------------
grunt.registerTask('server', "Run your server in development mode, auto-rebuilding when files change.", function(proxyMethod) {
var expressServerTask = 'expressServer:debug';
if (proxyMethod) {
expressServerTask += ':' + proxyMethod;
}
grunt.task.run(['clean:debug',
'build:debug',
expressServerTask,
'watch'
]);
});
grunt.registerTask('server:dist', "Build and preview a minified & production-ready version of your app.", [
'dist',
'expressServer:dist:keepalive'
]);
// Worker tasks
// =================================
grunt.registerTask('build:dist', filterAvailable([
'createResultDirectory', // Create directoy beforehand, fixes race condition
'fancySprites:create',
'concurrent:buildDist', // Executed in parallel, see config below
]));
grunt.registerTask('build:debug', filterAvailable([
'jshint:tooling',
'createResultDirectory', // Create directoy beforehand, fixes race condition
'fancySprites:create',
'concurrent:buildDebug', // Executed in parallel, see config below
]));
grunt.registerTask('createDistVersion', filterAvailable([
'useminPrepare', // Configures concat, cssmin and uglify
'concat', // Combines css and javascript files
//'cssmin', // Minifies css
//'uglify', // Minifies javascript
//'imagemin', // Optimizes image compression
// 'svgmin',
'copy:dist', // Copies files not covered by concat and imagemin
//'rev', // Appends 8 char hash value to filenames
//'usemin', // Replaces file references
//'htmlmin:dist' // Removes comments and whitespace
]));
// Documentation
// -------
grunt.registerTask('docs', "Build YUIDoc documentation.", [
'buildDocs',
'server:debug'
]);
// Parallelize most of the build process
_.merge(config, {
concurrent: {
buildDist: [
"buildTemplates:dist",
"buildScripts",
"buildStyles",
"buildIndexHTML:dist"
],
buildDebug: [
"buildTemplates:debug",
"buildScripts",
"buildStyles",
"buildIndexHTML:debug"
]
}
});
// Templates
grunt.registerTask('buildTemplates:dist', filterAvailable([
'emblem:compile',
'emberTemplates:dist'
]));
grunt.registerTask('buildTemplates:debug', filterAvailable([
'emblem:compile',
'emberTemplates:debug'
]));
// Scripts
grunt.registerTask('buildScripts', filterAvailable([
'jshint:app',
'validate-imports:app',
'coffee',
'emberscript',
'copy:javascriptToTmp',
'transpile',
'buildDocs',
'concat_sourcemap'
]));
// Styles
grunt.registerTask('buildStyles', filterAvailable([
'compass:compile',
'sass:compile',
'less:compile',
'stylus:compile',
'copy:cssToResult',
'autoprefixer:app'
]));
// Documentation
grunt.registerTask('buildDocs', filterAvailable([
'yuidoc:debug',
]));
// Index HTML
grunt.registerTask('buildIndexHTML:dist', [
'preprocess:indexHTMLDistApp',
'preprocess:indexHTMLDistTests'
]);
grunt.registerTask('buildIndexHTML:debug', [
'preprocess:indexHTMLDebugApp',
'preprocess:indexHTMLDebugTests'
]);
grunt.registerTask('createResultDirectory', function() {
grunt.file.mkdir('tmp/result');
});
grunt.initConfig(config);
};

View File

@ -0,0 +1,120 @@
API Stub
========
The stub allows you to implement express routes to fake API calls.
Simply add API routes in the routes.js file. The benefit of an API
stub is that you can use the REST adapter from the get go. It's a
way to use fixtures without having to use the fixture adapter.
As development progresses, the API stub becomes a functioning spec
for the real backend. Once you have a separate development API
server running, then switch from the stub to the proxy pass through.
To configure which API method to use edit **package.json**.
* Set the **APIMethod** to 'stub' to use these express stub routes.
* Set the method to 'proxy' and define the **proxyURL** to pass all API requests to the proxy URL.
Default Example
----------------
1. Create the following models:
app/models/post.js
```
var attr = DS.attr,
hasMany = DS.hasMany,
belongsTo = DS.belongsTo;
var Post = DS.Model.extend({
title: attr(),
comments: hasMany('comment'),
user: attr(),
});
export default Post;
```
app/models/comment.js
```
var attr = DS.attr,
hasMany = DS.hasMany,
belongsTo = DS.belongsTo;
var Comment = DS.Model.extend({
body: attr()
});
export default Comment;
```
2. Setup the REST adapter for the application:
app/adapters/application.js
```
var ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'api'
});
export default ApplicationAdapter;
```
3. Tell the Index router to query for a post:
app/routes/index.js
```
var IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('post', 1);
}
});
export default IndexRoute;
```
4. Expose the model properties in the index.hbs template
app/templates/index.hbs
```
<h2>{{title}}</h2>
<p>{{body}}</p>
<section class="comments">
<ul>
{{#each comment in comments}}
<li>
<div>{{comment.body}}</div>
</li>
{{/each}}
</ul>
</section>
```
When Ember Data queries the store for the post, it will make an API call to
http://localhost:8000/api/posts/1, to which the express server will respond with
some mock data:
```
{
"post": {
"id": 1,
"title": "Rails is omakase",
"comments": ["1", "2"],
"user" : "dhh"
},
"comments": [{
"id": "1",
"body": "Rails is unagi"
}, {
"id": "2",
"body": "Omakase O_o"
}]
}
```

View File

@ -0,0 +1,29 @@
module.exports = function(server) {
// Create an API namespace, so that the root does not
// have to be repeated for each end point.
server.namespace('/api', function() {
// Return fixture data for '/api/posts/:id'
server.get('/posts/:id', function(req, res) {
var post = {
"post": {
"id": 1,
"title": "Rails is omakase",
"comments": ["1", "2"],
"user" : "dhh"
},
"comments": [{
"id": "1",
"body": "Rails is unagi"
}, {
"id": "2",
"body": "Omakase O_o"
}]
};
res.send(post);
});
});
};

11
manager-client/app/app.js Normal file
View File

@ -0,0 +1,11 @@
import Resolver from 'ember/resolver';
import loadInitializers from 'ember/load-initializers';
var App = Ember.Application.extend({
modulePrefix: 'docker-manager', // TODO: loaded via config
Resolver: Resolver
});
loadInitializers(App, 'docker-manager');
export default App;

View File

View File

@ -0,0 +1,13 @@
export default Em.Component.extend({
classNameBindings: [':progress'],
barStyle: function() {
var percent = parseInt(this.get('percent'), 10);
if (percent > 0) {
if (percent > 100) { percent = 100; }
return 'width: ' + this.get('percent') + '%';
}
}.property('percent')
});

View File

@ -0,0 +1,8 @@
export default Em.Component.extend({
tagName: 'li',
classNameBindings: ['active'],
active: function() {
return this.get('childViews').anyBy('active');
}.property('childViews.@each.active')
});

View File

View File

@ -0,0 +1,4 @@
export default Em.ObjectController.extend({
upgrading: null
});

View File

@ -0,0 +1,20 @@
export default Ember.ObjectController.extend({
autoRefresh: false,
init: function() {
this._super();
var self = this;
window.setInterval(function() {
self.performRefresh();
}, 5000);
},
performRefresh: function() {
if (this.get('autoRefresh')) {
this.get('model').refresh();
}
}
});

View File

@ -0,0 +1,19 @@
export default Em.ObjectController.extend({
needs: ['index'],
upgradingRepo: Em.computed.alias('controllers.index.upgrading'),
managerRepo: Em.computed.alias('controllers.index.managerRepo'),
upgradeDisabled: function() {
var upgradingRepo = this.get('upgradingRepo');
if (Em.isNone(upgradingRepo)) {
var managerRepo = this.get('managerRepo');
if (!managerRepo) { return false; }
return (!managerRepo.get('upToDate')) && managerRepo !== this.get('model');
}
return true;
}.property('upgradingRepo', 'model', 'managerRepo', 'managerRepo.upToDate')
});

View File

@ -0,0 +1,79 @@
/* global MessageBus, bootbox */
export default Em.ObjectController.extend({
init: function() {
this._super();
this.reset();
},
complete: Em.computed.equal('status', 'complete'),
failed: Em.computed.equal('status', 'failed'),
messageReceived: function(msg) {
switch(msg.type) {
case "log":
this.set('output', this.get('output') + msg.value + "\n");
break;
case "percent":
this.set('percent', msg.value);
break;
case "status":
this.set('status', msg.value);
if (msg.value === 'complete' || msg.value === 'failed') {
this.set('upgrading', false);
}
if (msg.value === 'complete') {
this.set('version', this.get('latest.version'));
}
break;
}
},
upgradeButtonText: function() {
if (this.get('upgrading')) {
return "Upgrading...";
} else {
return "Start Upgrading";
}
}.property('upgrading'),
startBus: function() {
var self = this;
MessageBus.subscribe("/docker/upgrade", function(msg) {
self.messageReceived(msg);
});
},
stopBus: function() {
MessageBus.unsubscribe("/docker/upgrade");
},
reset: function() {
this.setProperties({ output: '', status: null, percent: 0 });
},
actions: {
start: function() {
this.reset();
var repo = this.get('model');
if (repo.get('upgrading')) { return; }
repo.startUpgrade();
},
resetUpgrade: function() {
var self = this;
bootbox.confirm("<p><b>WARNING:</b> You should only reset upgrades that have failed and are not running.</p> <p>This will NOT cancel currently running builds and should only be used as a last resort.</p>", function(cancel) {
if (cancel) {
var repo = self.get('model');
repo.resetUpgrade().then(function() {
self.reset();
});
}
});
}
},
});

View File

View File

@ -0,0 +1,4 @@
export default Em.Handlebars.makeBoundHelper(function(sha1, url) {
if (Em.isNone(url)) { return; }
return new Em.Handlebars.SafeString("(<a href='" + url + "'>" + sha1 + "</a>)");
});

View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Docker Manager</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- build:css(tmp/result) /assets/app.min.css -->
<link rel="stylesheet" href="/assets/app.css">
<link rel="stylesheet" href="/vendor/bootstrap/dist/css/bootstrap.css">
<!-- endbuild -->
<!-- for more details visit: https://github.com/yeoman/grunt-usemin -->
<!-- build:js(tmp/result) /assets/config.min.js -->
<script src="/config/environment.js"></script>
<!-- @if dist=false -->
<script src="/config/environments/development.js"></script>
<!-- @endif --><!-- @if dist=true -->
<script src="/config/environments/production.js"></script>
<!-- @endif -->
<!-- endbuild -->
<!-- build:js(tmp/result) /assets/vendor.min.js -->
<script src="/vendor/jquery/jquery.js"></script>
<!-- @if dist=false -->
<script src="/vendor/handlebars/handlebars.js"></script>
<script src="/vendor/ember/ember.js"></script>
<!-- @endif --><!-- @if dist=true -->
<script src="/vendor/handlebars/handlebars.runtime.js"></script>
<script src="/vendor/ember/ember.prod.js"></script>
<!-- @endif -->
<script src="/vendor/loader.js/loader.js"></script>
<script src="/vendor/ember-resolver/dist/ember-resolver.js"></script>
<script src="/vendor/ember-shim.js"></script>
<script src="/vendor/ic-ajax/dist/named-amd/main.js"></script>
<script src="/vendor/ember-load-initializers/ember-load-initializers.js"></script>
<script src="/vendor/message-bus/assets/message-bus.js"></script>
<script src="/vendor/bootstrap/js/modal.js"></script>
<script src="/vendor/bootbox/bootbox.js"></script>
<!-- endbuild -->
<!-- build:js(tmp/result) /assets/app.min.js -->
<script src="/assets/app.js"></script>
<script src="/assets/templates.js"></script>
<!-- endbuild -->
</head>
<body>
<script>
window.App = require('docker-manager/app')['default'].create(ENV.APP);
</script>
</body>
</html>

View File

View File

@ -0,0 +1,16 @@
import ajax from "ic-ajax";
export default {
name: "findCsrfToken",
initialize: function(container, application) {
return ajax('/session/csrf').then(function(result) {
var token = result.csrf;
$.ajaxPrefilter(function(options, originalOptions, xhr) {
if (!options.crossDomain) {
xhr.setRequestHeader('X-CSRF-Token', token);
}
});
});
}
};

View File

View File

@ -0,0 +1,26 @@
import ajax from "ic-ajax";
var ProcessList = Em.Object.extend({
init: function() {
this._super();
},
refresh: function() {
var self = this;
return ajax("/admin/docker/ps").then(function(result) {
self.set('output', result);
return self;
});
}
});
ProcessList.reopenClass({
find: function() {
var list = ProcessList.create();
return list.refresh();
}
});
export default ProcessList;

View File

@ -0,0 +1,99 @@
import ajax from "ic-ajax";
var loaded = [];
var Repo = Em.Object.extend({
upToDate: function() {
return this.get('version') === this.get('latest.version');
}.property('version', 'latest.version'),
shouldCheck: function() {
if (Em.isNone(this.get('version'))) { return false; }
if (this.get('checking')) { return false; }
// Only check once every minute
var lastCheckedAt = this.get('lastCheckedAt');
if (lastCheckedAt) {
var ago = new Date().getTime() - lastCheckedAt;
return ago > 60 * 1000;
}
return true;
}.property().volatile(),
repoAjax: function(url, args) {
args = args || {};
args.data = this.getProperties('path', 'version');
return ajax(url, args);
},
findLatest: function() {
var self = this;
return new Em.RSVP.Promise(function(resolve, reject) {
if (!self.get('shouldCheck')) { return resolve(); }
self.set('checking', true);
self.repoAjax('/admin/docker/latest').then(function(result) {
self.setProperties({
checking: false,
lastCheckedAt: new Date().getTime(),
latest: Em.Object.create(result.latest)
});
resolve();
});
});
},
findProgress: function() {
return this.repoAjax('/admin/docker/progress').then(function(result) {
return result.progress;
});
},
resetUpgrade: function() {
var self = this;
return this.repoAjax('/admin/docker/upgrade', { type: 'DELETE' }).then(function() {
self.set('upgrading', false);
});
},
startUpgrade: function() {
var self = this;
this.set('upgrading', true);
return this.repoAjax('/admin/docker/upgrade', { type: 'POST' }).catch(function() {
self.set('upgrading', false);
});
}
});
Repo.reopenClass({
findAll: function() {
return new Em.RSVP.Promise(function (resolve) {
if (loaded.length) { return resolve(loaded); }
ajax("/admin/docker/repos").then(function(result) {
loaded = result.repos.map(function(r) {
return Repo.create(r);
});
resolve(loaded);
});
});
},
findUpgrading: function() {
return this.findAll().then(function(result) {
return result.findBy('upgrading', true);
});
},
find: function(id) {
return this.findAll().then(function(result) {
return result.findBy('id', id);
});
},
});
export default Repo;

View File

@ -0,0 +1,8 @@
var Router = Ember.Router.extend(); // ensure we don't share routes between all Router instances
Router.map(function() {
this.route("processes");
this.resource('upgrade', { path: '/upgrade/:id' });
});
export default Router;

View File

View File

@ -0,0 +1,30 @@
import Repo from 'docker-manager/models/repo';
export default Em.Route.extend({
model: function() {
return Repo.findAll();
},
setupController: function(controller, model) {
controller.setProperties({ model: model, upgrading: null });
model.forEach(function(repo) {
repo.findLatest();
if (repo.get('upgrading')) {
controller.set('upgrading', repo);
}
// Special case: Upgrade docker manager first
if (repo.get('id') === 'docker_manager') {
controller.set('managerRepo', repo);
}
});
},
actions: {
upgrade: function(repo) {
this.transitionTo('upgrade', repo);
}
}
});

View File

@ -0,0 +1,7 @@
import ProcessList from 'docker-manager/models/process-list';
export default Em.Route.extend({
model: function() {
return ProcessList.find();
}
});

View File

@ -0,0 +1,38 @@
import Repo from 'docker-manager/models/repo';
export default Em.Route.extend({
model: function(params) {
return Repo.find(params.id);
},
afterModel: function(model, transition) {
var self = this;
return Repo.findUpgrading().then(function(u) {
if (u && u !== model) {
return Ember.RSVP.Promise.reject("wat");
}
return model.findLatest().then(function() {
return model.findProgress().then(function(progress) {
self.set("progress", progress);
});
});
});
},
setupController: function(controller, model) {
controller.reset();
controller.setProperties({
model: model,
output: this.get('progress.logs'),
percent: this.get('progress.percentage')
});
controller.startBus();
},
deactivate: function() {
this.controllerFor('upgrade').stopBus();
}
});

View File

View File

@ -0,0 +1,83 @@
/* Put your CSS here */
html{
margin: 20px;
overflow-y: scroll;
overflow-x: auto;
}
header {
img.logo {
width: 200px;
float: left;
}
a[href] {
text-decoration: none;
color: black;
}
h1 {
text-align: right;
}
clear: both;
}
div.logs {
margin-top: 20px;
overflow-x: scroll;
height: 400px;
white-space: pre;
font-family: monospace;
background-color: black;
color: #ddd;
padding: 10px;
}
table#repos {
margin-top: 10px;
}
h3.loading {
display: none;
}
.new-version {
h4 {
margin: 0 0 10px 0;
font-weight: bold;
font-size: 14px;
}
ul {
list-style: none;
padding: 0;
}
li.new-commits {
font-style: italic;
}
button {
display: block;
padding: 2px 5px;
margin: 10px 0 0px 0;
}
}
button.btn {
background: #00aaff;
color: white !important;
border-radius: 0;
&:hover {
background-color: #0081c2;
}
}
button.unlock {
float: right;
background-color: red;
&:hover {
background-color: #a00;
}
}

View File

View File

@ -0,0 +1,15 @@
<header class="container">
{{#link-to 'index'}}<img src="/assets/images/docker-manager-ea64623b074c8ec2b0303bae846e21e6.png" class="logo">{{/link-to}}
<h1>{{#link-to 'index'}}Docker Manager{{/link-to}}</h1>
</header>
<div class="container">
<ul class="nav nav-tabs">
{{#x-tab route="index"}}Home{{/x-tab}}
{{#x-tab route="processes"}}Processes{{/x-tab}}
</ul>
{{outlet}}
</div>

View File

@ -0,0 +1 @@
<div class="progress-bar" {{bind-attr style="barStyle"}}></div>

View File

@ -0,0 +1 @@
{{#link-to route}}{{yield}}{{/link-to}}

View File

@ -0,0 +1,41 @@
<h3>Repositories</h3>
<table class='table' id='repos'>
<tr>
<th style='width: 50%'>Name</th>
<th>Status</th>
</tr>
<tbody>
{{#each model itemController="repo"}}
<tr>
<td>
{{name}}
{{fmt-commit version url}}
</td>
<td>
{{#if checking}}
Checking for new version...
{{else}}
{{#if upToDate}}
Up to date
{{else}}
<div class='new-version'>
<h4>New Version Available!</h4>
<ul>
<li>Remote Version: {{fmt-commit latest.version url}}</li>
<li>Last Updated: {{latest.date}}</li>
<li class='new-commits'>{{latest.commits_behind}} new commits</li>
</ul>
{{#if upgrading}}
<button class="btn" {{action upgrade this}}>Currently Upgrading...</button>
{{else}}
<button class="btn" {{action upgrade this}} {{bind-attr disabled="upgradeDisabled"}}>Upgrade to the Latest Version</button>
{{/if}}
</div>
{{/if}}
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>

View File

@ -0,0 +1 @@
<h3 class='loading'>Loading...</h3>

View File

@ -0,0 +1,3 @@
<h3>Processes</h3>
<div class='logs'>{{output}}</div>

View File

@ -0,0 +1,26 @@
<h3>Upgrade {{name}}</h3>
{{progress-bar percent=percent}}
{{#if complete}}
<p>Upgrade completed successfully!</p>
<p>Note: The web server restarts in the background. It's a good idea to wait 30 seconds or so
before refreshing your browser to see the latest version of the application.</p>
{{/if}}
{{#if failed}}
<p>Sorry, there wasn an error upgrading Discourse. Please check the logs.</p>
{{/if}}
{{#if upToDate}}
<p>{{name}} is at the newest version {{fmt-commit version url}}.</p>
{{else}}
<div style='clear: both'>
<button {{action start}} {{bind-attr disabled="upgrading"}} class='btn'>{{upgradeButtonText}}</button>
{{#if upgrading}}
<button {{action resetUpgrade}} class="btn unlock">Reset Upgrade</button>
{{/if}}
</div>
{{/if}}
<div class='logs'>{{output}}</div>

View File

View File

@ -0,0 +1,4 @@
/* global ic */
export default function ajax(){
return ic.ajax.apply(null, arguments);
}

View File

View File

@ -0,0 +1,13 @@
export default Em.View.extend({
_showOnInsert: function() {
var self = this;
self.set('runner', Em.run.later(function() {
self.$('h3').show();
}, 200));
}.on('didInsertElement'),
_cancelFade: function() {
Em.run.cancel(this.get('runner'));
}.on('willDestroyElement')
});

View File

@ -0,0 +1,12 @@
export default Em.View.extend({
_insertedIntoDOM: function() {
this.set('controller.autoRefresh', true);
}.on('didInsertElement'),
_removedFromDOM: function() {
this.set('controller.autoRefresh', false);
}.on('willDestroyElement')
});

14
manager-client/bower.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "ember-app-kit",
"dependencies": {
"bootstrap": "~3.1.1",
"handlebars": "~1.1.2",
"jquery": "~1.9.1",
"ember": "~1.5.0",
"ember-resolver": "git://github.com/stefanpenner/ember-jj-abrams-resolver.git#master",
"ic-ajax": "~1.0.4",
"loader.js": "git://github.com/stefanpenner/loader.js",
"ember-load-initializers": "git://github.com/stefanpenner/ember-load-initializers.git#0.0.1",
"message-bus": "https://raw.githubusercontent.com/SamSaffron/message_bus.git"
}
}

View File

@ -0,0 +1,12 @@
// Put general configuration here. This file is included
// in both production and development BEFORE Ember is
// loaded.
//
// For example to enable a feature on a canary build you
// might do:
//
// window.ENV = {FEATURES: {'with-controller': true}};
window.ENV = window.ENV || {};
window.ENV.MODEL_FACTORY_INJECTIONS = true;

View File

@ -0,0 +1,14 @@
// Put your development configuration here.
//
// This is useful when using a separate API
// endpoint in development than in production.
//
// window.ENV.public_key = '123456'
window.ENV.APP = {
LOG_ACTIVE_GENERATION: true,
LOG_MODULE_RESOLVER: true,
LOG_TRANSITIONS: true,
LOG_TRANSITIONS_INTERNAL: true,
LOG_VIEW_LOOKUPS: true
};

View File

@ -0,0 +1,6 @@
// Put your production configuration here.
//
// This is useful when using a separate API
// endpoint in development than in production.
//
// window.ENV.public_key = '123456'

View File

@ -0,0 +1,6 @@
// Put your test configuration here.
//
// This is useful when using a separate API
// endpoint in test than in production.
//
// window.ENV.public_key = '123456'

View File

@ -0,0 +1,57 @@
{
"name": "docker-manager",
"namespace": "docker-manager",
"APIMethod": "proxy",
"proxyURL": "http://localhost:3000",
"proxyPath": "/admin/docker",
"docURL": "http://localhost:8000/docs/index.html",
"version": "0.0.0",
"private": true,
"directories": {
"doc": "doc",
"test": "test"
},
"scripts": {
"start": "grunt server",
"build": "grunt build:debug",
"test": "grunt test:ci",
"postinstall": "bower install"
},
"repository": {
"type": "git",
"url": "git://github.com/stefanpenner/ember-app-kit.git"
},
"author": "",
"license": "MIT",
"devDependencies": {
"express": "~3.4.8",
"lockfile": "~0.4.2",
"bower": "~1.3.2",
"grunt": "~0.4.2",
"grunt-cli": "~0.1.9",
"load-grunt-config": "~0.7.0",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-jshint": "~0.8.0",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-cssmin": "~0.6.2",
"grunt-preprocess": "~3.0.1",
"grunt-es6-module-transpiler": "~0.6.0",
"grunt-concat-sourcemap": "~0.4.0",
"grunt-concurrent": "~0.4.3",
"grunt-usemin": "~0.1.13",
"grunt-rev": "~0.1.0",
"grunt-ember-templates": "~0.4.18",
"grunt-contrib-testem": "~0.5.14",
"express-namespace": "~0.1.1",
"request": "~2.33.0",
"loom-generators-ember-appkit": "~1.0.5",
"originate": "~0.1.5",
"loom": "~3.1.2",
"connect-livereload": "~0.3.1",
"grunt-es6-import-validate": "0.0.6",
"grunt-sass": "~0.12.1"
}
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
<!-- Most restrictive policy: -->
<site-control permitted-cross-domain-policies="none"/>
<!-- Least restrictive policy: -->
<!--
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
-->
</cross-domain-policy>

View File

@ -0,0 +1,15 @@
# humanstxt.org/
# The humans responsible & technology colophon
# TEAM
<name> -- <role> -- <twitter>
# THANKS
<name>
# TECHNOLOGY COLOPHON
HTML5, CSS3
Ember.js

View File

@ -0,0 +1,3 @@
# robotstxt.org/
User-agent: *

View File

@ -0,0 +1,19 @@
/**
* This is dummy file that exists for the sole purpose
* of allowing tests to run directly in the browser as
* well as by Testem.
*
* Testem is configured to run tests directly against
* the test build of index.html, which requires a
* snippet to load the testem.js file:
* <script src="/testem.js"></script>
* This has to go after the qunit framework and app
* tests are loaded.
*
* Testem internally supplies this file. However, if you
* run the tests directly in the browser (localhost:8000/tests),
* this file does not exist.
*
* Hence the purpose of this fake file. This file is served
* directly from the express server to satisify the script load.
*/

View File

@ -0,0 +1,27 @@
{
"node" : true,
"browser" : false,
"boss" : true,
"curly": false,
"debug": false,
"devel": false,
"eqeqeq": true,
"evil": true,
"forin": false,
"immed": false,
"laxbreak": false,
"newcap": true,
"noarg": true,
"noempty": false,
"nonew": false,
"nomen": false,
"onevar": false,
"plusplus": false,
"regexp": false,
"undef": true,
"sub": true,
"strict": false,
"white": false,
"eqnull": true,
"esnext": true
}

View File

@ -0,0 +1,131 @@
module.exports = function(grunt) {
var express = require('express'),
lockFile = require('lockfile'),
Helpers = require('./helpers'),
fs = require('fs'),
path = require('path'),
request = require('request');
/**
Task for serving the static files.
Note: The expressServer:debug task looks for files in multiple directories.
*/
grunt.registerTask('expressServer', function(target, proxyMethodToUse) {
// Load namespace module before creating the server
require('express-namespace');
var app = express(),
done = this.async(),
proxyMethod = proxyMethodToUse || grunt.config('express-server.options.APIMethod');
app.use(lock);
app.use(express.compress());
if (proxyMethod === 'stub') {
grunt.log.writeln('Using API Stub');
// Load API stub routes
app.use(express.json());
app.use(express.urlencoded());
require('../api-stub/routes')(app);
} else if (proxyMethod === 'proxy') {
var proxyURL = grunt.config('express-server.options.proxyURL'),
proxyPath = grunt.config('express-server.options.proxyPath') || '/api';
grunt.log.writeln('Proxying API requests matching ' + proxyPath + '/* to: ' + proxyURL);
// Use API proxy
app.all(proxyPath + '/*', passThrough(proxyURL));
app.all('/message-bus/*', passThrough(proxyURL));
app.all('/session/*', passThrough(proxyURL));
}
if (target === 'debug') {
// For `expressServer:debug`
// Add livereload middleware after lock middleware if enabled
if (Helpers.isPackageAvailable("connect-livereload")) {
var liveReloadPort = grunt.config('watch.options.livereload');
app.use(require("connect-livereload")({port: liveReloadPort}));
}
// YUIDoc serves static HTML, so just serve the index.html
app.all('/docs', function(req, res) { res.redirect(302, '/docs/index.html'); });
app.use(static({ urlRoot: '/docs', directory: 'docs' }));
// These three lines simulate what the `copy:assemble` task does
app.use(static({ urlRoot: '/config', directory: 'config' }));
app.use(static({ urlRoot: '/vendor', directory: 'vendor' }));
app.use(static({ directory: 'public' }));
app.use(static({ urlRoot: '/tests', directory: 'tests' })); // For test-helper.js and test-loader.js
app.use(static({ directory: 'tmp/result' }));
app.use(static({ file: 'tmp/result/index.html', ignoredFileExtensions: /\.\w{1,5}$/ })); // Gotta catch 'em all
} else {
// For `expressServer:dist`
app.use(lock);
app.use(static({ directory: 'dist' }));
app.use(static({ file: 'dist/index.html', ignoredFileExtensions: /\.\w{1,5}$/ })); // Gotta catch 'em all
}
var port = parseInt(process.env.PORT || 8000, 10);
if (isNaN(port) || port < 1 || port > 65535) {
grunt.fail.fatal('The PORT environment variable of ' + process.env.PORT + ' is not valid.');
}
app.listen(port);
grunt.log.ok('Started development server on port %d.', port);
if (!this.flags.keepalive) { done(); }
});
// Middleware
// ==========
function lock(req, res, next) { // Works with tasks/locking.js
(function retry() {
if (lockFile.checkSync('tmp/connect.lock')) {
setTimeout(retry, 30);
} else { next(); }
})();
}
function static(options) {
return function(req, res, next) { // Gotta catch 'em all (and serve index.html)
var filePath = "";
if (options.directory) {
var regex = new RegExp('^' + (options.urlRoot || ''));
// URL must begin with urlRoot's value
if (!req.path.match(regex)) { next(); return; }
filePath = options.directory + req.path.replace(regex, '');
} else if (options.file) {
filePath = options.file;
} else { throw new Error('static() isn\'t properly configured!'); }
fs.stat(filePath, function(err, stats) {
if (err) { next(); return; } // Not a file, not a folder => can't handle it
if (options.ignoredFileExtensions) {
if (options.ignoredFileExtensions.test(req.path)) {
res.send(404, {error: 'Resource not found'});
return; // Do not serve index.html
}
}
// Is it a directory? If so, search for an index.html in it.
if (stats.isDirectory()) { filePath = path.join(filePath, 'index.html'); }
// Serve the file
res.sendfile(filePath, function(err) {
if (err) { next(); return; }
grunt.verbose.ok('Served: ' + filePath);
});
});
};
}
function passThrough(target) {
return function(req, res) {
req.pipe(request(target+req.url)).pipe(res);
};
}
};

View File

@ -0,0 +1,60 @@
var grunt = require('grunt'),
_ = grunt.util._,
Helpers = {};
// List of package requisits for tasks
// Notated in conjunctive normal form (CNF)
// e.g. ['a', ['b', 'alternative-to-b']]
var taskRequirements = {
coffee: ['grunt-contrib-coffee'],
compass: ['grunt-contrib-compass'],
sass: [['grunt-sass', 'grunt-contrib-sass']],
less: ['grunt-contrib-less'],
stylus: ['grunt-contrib-stylus'],
emberTemplates: ['grunt-ember-templates'],
emblem: ['grunt-emblem'],
emberscript: ['grunt-ember-script'],
imagemin: ['grunt-contrib-imagemin'],
htmlmin: ['grunt-contrib-htmlmin'],
fancySprites: ['grunt-fancy-sprites'],
autoprefixer: ['grunt-autoprefixer'],
rev: ['grunt-rev'],
'validate-imports': ['grunt-es6-import-validate'],
yuidoc: ['grunt-contrib-yuidoc']
};
// Task fallbacks
// e.g. 'a': ['fallback-a-step-1', 'fallback-a-step-2']
var taskFallbacks = {
'imagemin': 'copy:imageminFallback'
};
Helpers.filterAvailableTasks = function(tasks){
tasks = tasks.map(function(taskName) {
// Maps to task name or fallback if task is unavailable
var baseName = taskName.split(':')[0]; // e.g. 'coffee' for 'coffee:compile'
var reqs = taskRequirements[baseName];
var isAvailable = Helpers.isPackageAvailable(reqs);
return isAvailable ? taskName : taskFallbacks[taskName];
});
return _.flatten(_.compact(tasks)); // Remove undefined's and flatten it
};
Helpers.isPackageAvailable = function(pkgNames) {
if (!pkgNames) return true; // packages are assumed to exist
if (!_.isArray(pkgNames)) { pkgNames = [pkgNames]; }
return _.every(pkgNames, function(pkgNames) {
if (!_.isArray(pkgNames)) { pkgNames = [pkgNames]; }
return _.any(pkgNames, function(pkgName) {
return !!Helpers.pkg.devDependencies[pkgName];
});
});
};
module.exports = Helpers;

View File

@ -0,0 +1,14 @@
var lockFile = require('lockfile');
module.exports = function(grunt) {
grunt.registerTask('lock', 'Set semaphore for connect server to wait on.', function() {
grunt.file.mkdir('tmp');
lockFile.lockSync('tmp/connect.lock');
grunt.log.writeln("Locked - Development server won't answer incoming requests until App Kit is done updating.");
});
grunt.registerTask('unlock', 'Release semaphore that connect server waits on.', function() {
lockFile.unlockSync('tmp/connect.lock');
grunt.log.writeln("Unlocked - Development server now handles requests.");
});
};

View File

@ -0,0 +1,5 @@
module.exports = {
app: {
src: 'tmp/result/assets/**/*.css'
}
};

View File

@ -0,0 +1,4 @@
module.exports = {
'debug': ['tmp'],
'dist': ['tmp', 'dist']
};

View File

@ -0,0 +1,42 @@
// CoffeeScript compilation. This must be enabled by modification
// of Gruntfile.js.
//
// The `bare` option is used since this file will be transpiled
// anyway. In CoffeeScript files, you need to escape out for
// some ES6 features like import and export. For example:
//
// `import User from 'appkit/models/user'`
//
// Post = Em.Object.extend
// init: (userId) ->
// @set 'user', User.findById(userId)
//
// `export default Post`
//
module.exports = {
"test": {
options: {
bare: true
},
files: [{
expand: true,
cwd: 'tests/',
src: '**/*.coffee',
dest: 'tmp/javascript/tests',
ext: '.js'
}]
},
"app": {
options: {
bare: true
},
files: [{
expand: true,
cwd: 'app/',
src: '**/*.coffee',
dest: 'tmp/javascript/app',
ext: '.js'
}]
}
};

View File

@ -0,0 +1,17 @@
module.exports = {
options: {
sassDir: "app/styles",
cssDir: "tmp/result/assets",
generatedImagesDir: "tmp/result/assets/images/generated",
imagesDir: "public/assets/images",
javascriptsDir: "app",
fontsDir: "public/assets/fonts",
importPath: ["vendor"],
httpImagesPath: "/assets/images",
httpGeneratedImagesPath: "/assets/images/generated",
httpFontsPath: "/assets/fonts",
relativeAssets: false,
debugInfo: true
},
compile: {}
};

View File

@ -0,0 +1,25 @@
module.exports = {
app: {
src: ['tmp/transpiled/app/**/*.js'],
dest: 'tmp/result/assets/app.js',
options: {
sourcesContent: true
},
},
config: {
src: ['tmp/result/config/**/*.js'],
dest: 'tmp/result/assets/config.js',
options: {
sourcesContent: true
},
},
test: {
src: 'tmp/transpiled/tests/**/*.js',
dest: 'tmp/result/tests/tests.js',
options: {
sourcesContent: true
}
}
};

View File

@ -0,0 +1,6 @@
module.exports = {
// Remaining configuration done in Gruntfile.js
options: {
logConcurrentOutput: true
}
};

View File

@ -0,0 +1,72 @@
module.exports = {
// Note: These tasks are listed in the order in which they will run.
javascriptToTmp: {
files: [{
expand: true,
cwd: 'app',
src: '**/*.js',
dest: 'tmp/javascript/app'
},
{
expand: true,
cwd: 'tests',
src: ['**/*.js', '!test-helper.js', '!test-loader.js'],
dest: 'tmp/javascript/tests/'
}]
},
cssToResult: {
expand: true,
cwd: 'app/styles',
src: ['**/*.css'],
dest: 'tmp/result/assets'
},
// Assembles everything in `tmp/result`.
// The sole purpose of this task is to keep things neat. Gathering everything in one
// place (tmp/dist) enables the subtasks of dist to only look there. Note: However,
// for normal development this is done on the fly by the development server.
assemble: {
files: [{
expand: true,
cwd: 'tests',
src: ['test-helper.js', 'test-loader.js'],
dest: 'tmp/result/tests/'
}, {
expand: true,
cwd: 'public',
src: ['**'],
dest: 'tmp/result/'
}, {
src: ['vendor/**/*.js', 'vendor/**/*.css'],
dest: 'tmp/result/'
}, {
src: ['config/environment.js', 'config/environments/production.js'],
dest: 'tmp/result/'
}
]
},
imageminFallback: {
files: '<%= imagemin.dist.files %>'
},
dist: {
files: [{
expand: true,
cwd: 'tmp/result',
src: [
'**',
'!**/*.{css,js}', // Already handled by concat
'!**/*.{png,gif,jpg,jpeg}', // Already handled by imagemin
'!tests/**/*', // No tests, please
'!**/*.map' // No source maps
],
filter: 'isFile',
dest: 'dist/'
}]
},
};

View File

@ -0,0 +1,22 @@
var grunt = require('grunt');
module.exports = {
options: {
templateBasePath: /app\//,
templateFileExtensions: /\.(hbs|hjs|handlebars)/,
templateRegistration: function(name, template) {
return grunt.config.process("define('<%= package.namespace %>/") + name + "', ['exports'], function(__exports__){ __exports__['default'] = " + template + "; });";
}
},
debug: {
options: {
precompile: false
},
src: "app/**/*.{hbs,hjs,handlebars}",
dest: "tmp/result/assets/templates.js"
},
dist: {
src: "<%= emberTemplates.debug.src %>",
dest: "<%= emberTemplates.debug.dest %>"
}
};

View File

@ -0,0 +1,42 @@
// EmberScript compilation. This must be enabled by modification
// of Gruntfile.js.
//
// The `bare` option is used since this file will be transpiled
// anyway. In EmberScript files, you need to escape out for
// some ES6 features like import and export. For example:
//
// `import User from 'appkit/models/user'`
//
// class Post
// init: (userId) ->
// @user = User.findById(userId)
//
// `export default Post`
//
module.exports = {
"test": {
options: {
bare: true
},
files: [{
expand: true,
cwd: 'tests/',
src: '**/*.em',
dest: 'tmp/javascript/tests',
ext: '.js'
}]
},
"app": {
options: {
bare: true
},
files: [{
expand: true,
cwd: 'app/',
src: '**/*.em',
dest: 'tmp/javascript/app',
ext: '.js'
}]
}
};

View File

@ -0,0 +1,16 @@
module.exports = {
compile: {
files: {
"tmp/result/assets/templates.js": ['app/templates/**/*.{emblem,hbs,hjs,handlebars}']
},
options: {
root: 'app/templates/',
dependencies: {
jquery: 'vendor/jquery/jquery.js',
ember: 'vendor/ember/ember.js',
handlebars: 'vendor/handlebars/handlebars.js',
emblem: 'vendor/emblem.js/emblem.js'
}
}
}
};

View File

@ -0,0 +1,9 @@
var grunt = require('grunt');
module.exports = {
options: {
APIMethod: "<%= package.APIMethod %>", // stub or proxy
proxyURL: "<%= package.proxyURL %>", // URL to the API server, if using APIMethod: 'proxy'
proxyPath: "<%= package.proxyPath %>" // path for the API endpoints, default is '/api', if using APIMethod: 'proxy'
}
};

View File

@ -0,0 +1,22 @@
var path = require('path');
module.exports = {
create: {
destStyles: 'tmp/sprites',
destSpriteSheets: 'tmp/result/assets/sprites',
files: [{
src: ['app/sprites/**/*.{png,jpg,jpeg}', '!app/sprites/**/*@2x.{png,jpg,jpeg}'],
spriteSheetName: '1x',
spriteName: function(name) {
return path.basename(name, path.extname(name));
}
}, {
src: 'app/sprites/**/*@2x.{png,jpg,jpeg}',
spriteSheetName: '2x',
spriteName: function(name) {
return path.basename(name, path.extname(name)).replace(/@2x/, '');
}
}
]
}
};

View File

@ -0,0 +1,12 @@
module.exports = {
dist: {
options: {
removeComments: true,
collapseWhitespace: true
},
files: [{
src: 'dist/index.html',
dest: 'dist/index.html'
}]
}
};

View File

@ -0,0 +1,13 @@
module.exports = {
dist: {
options: {
cache: false
},
files: [{
expand: true,
cwd: 'tmp/result',
src: '**/*.{png,gif,jpg,jpeg}',
dest: 'dist/'
}]
}
};

View File

@ -0,0 +1,27 @@
module.exports = {
app: {
src: [
'app/**/*.js'
],
options: { jshintrc: '.jshintrc' }
},
tooling: {
src: [
'Gruntfile.js',
'tasks/**/*.js'
],
options: { jshintrc: 'tasks/.jshintrc' }
},
tests: {
src: [
'tests/**/*.js',
],
options: { jshintrc: 'tests/.jshintrc' }
},
options: {
force: true
}
};

View File

@ -0,0 +1,11 @@
module.exports = {
compile: {
files: [{
expand: true,
cwd: 'app/styles',
src: ['**/*.less', '!**/_*.less'],
dest: 'tmp/result/assets/',
ext: '.css'
}]
}
};

View File

@ -0,0 +1,18 @@
module.exports = {
indexHTMLDebugApp: {
src : 'app/index.html', dest : 'tmp/result/index.html',
options: { context: { dist: false, tests: false } }
},
indexHTMLDebugTests: {
src : 'app/index.html', dest : 'tmp/result/tests/index.html',
options: { context: { dist: false, tests: true } }
},
indexHTMLDistApp: {
src : 'app/index.html', dest : 'tmp/result/index.html',
options: { context: { dist: true, tests: false } }
},
indexHTMLDistTests: {
src : 'app/index.html', dest : 'tmp/result/tests/index.html',
options: { context: { dist: true, tests: true } }
}
};

View File

@ -0,0 +1,12 @@
module.exports = {
dist: {
files: {
src: [
'dist/assets/config.min.js',
'dist/assets/app.min.js',
'dist/assets/vendor.min.js',
'dist/assets/app.min.css'
]
}
}
};

View File

@ -0,0 +1,11 @@
module.exports = {
compile: {
files: [{
expand: true,
cwd: 'app/styles',
src: ['**/*.{scss,sass}', '!**/_*.{scss,sass}'],
dest: 'tmp/result/assets/',
ext: '.css'
}]
}
};

View File

@ -0,0 +1,11 @@
module.exports = {
compile: {
files: [{
expand: true,
cwd: 'app/styles',
src: ['**/*.styl', '!**/_*.styl'],
dest: 'tmp/result/assets/',
ext: '.css'
}]
}
};

View File

@ -0,0 +1,55 @@
// See https://npmjs.org/package/grunt-contrib-testem for more config options
module.exports = {
basic: {
options: {
parallel: 2,
framework: 'qunit',
port: (parseInt(process.env.PORT || 7358, 10) + 1),
test_page: 'tmp/result/tests/index.html',
routes: {
'/tests/tests.js': 'tmp/result/tests/tests.js',
'/assets/app.js': 'tmp/result/assets/app.js',
'/assets/templates.js': 'tmp/result/assets/templates.js',
'/assets/app.css': 'tmp/result/assets/app.css'
},
src_files: [
'tmp/result/**/*.js'
],
launch_in_dev: ['PhantomJS', 'Chrome'],
launch_in_ci: ['PhantomJS', 'Chrome'],
}
},
browsers: {
options: {
parallel: 8,
framework: 'qunit',
port: (parseInt(process.env.PORT || 7358, 10) + 1),
test_page: 'tmp/result/tests/index.html',
routes: {
'/tests/tests.js': 'tmp/result/tests/tests.js',
'/assets/app.js': 'tmp/result/assets/app.js',
'/assets/templates.js': 'tmp/result/assets/templates.js',
'/assets/app.css': 'tmp/result/assets/app.css'
},
src_files: [
'tmp/result/**/*.js'
],
launch_in_dev: ['PhantomJS',
'Chrome',
'ChromeCanary',
'Firefox',
'Safari',
'IE7',
'IE8',
'IE9'],
launch_in_ci: ['PhantomJS',
'Chrome',
'ChromeCanary',
'Firefox',
'Safari',
'IE7',
'IE8',
'IE9'],
}
}
};

View File

@ -0,0 +1,28 @@
var grunt = require('grunt');
module.exports = {
"tests": {
type: 'amd',
moduleName: function(path) {
return grunt.config.process('<%= package.namespace %>/tests/') + path;
},
files: [{
expand: true,
cwd: 'tmp/javascript/tests/',
src: '**/*.js',
dest: 'tmp/transpiled/tests/'
}]
},
"app": {
type: 'amd',
moduleName: function(path) {
return grunt.config.process('<%= package.namespace %>/') + path;
},
files: [{
expand: true,
cwd: 'tmp/javascript/app/',
src: '**/*.js',
dest: 'tmp/transpiled/app/'
}]
}
};

View File

@ -0,0 +1,3 @@
module.exports = {
html: ['dist/index.html'],
};

View File

@ -0,0 +1,6 @@
module.exports = {
html: 'tmp/result/index.html',
options: {
dest: 'dist/'
}
};

Some files were not shown because too many files have changed in this diff Show More