Ember upgrade to 3.6.1 (#49)

* Ember upgrade to 3.6.1

* ember-ajax printing deprecations; stop using it

* Use tag versions when possible instead of hashes

* Remove inline scripts for CSP

* dump `git describe` errors

* Commit hashes still needed for github diff link

* Update READMEs

* Production build

* Make repo-status component more isolated

* Minor tweaks

* Get JS tests in a working state, and write tests for repo-status component

* Add a note about tests in README

* Apply Prettier

* Production build #2

* We need vendor/message-bus.js
This commit is contained in:
Osama Sayegh 2019-01-03 09:21:34 +03:00 committed by GitHub
parent a929416aa2
commit 1309955216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 15028 additions and 13531 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
*.swp *.swp
manager-client/vendor/*
*.sublime-workspace *.sublime-workspace
.DS_Store .DS_Store
._.DS_Store ._.DS_Store

View File

@ -4,9 +4,9 @@ This plugin works with the Discourse docker image. It allows you to perform upgr
## Development Notes ## Development Notes
* Install `node.js`, `bower`, `grunt` * Install `node.js` and `yarn`
* Clone this repo to desired folder path * Clone this repo to desired folder path
* In console, from folder path do `cd manager-client`, `npm install`, `bower install` * In console, from folder path do `cd manager-client`, `yarn install`
* Create a symlink for this folder in your local Discourse instance "plugins" folder (eg. `path/to/your/discourse_folder/plugins/discourse_manager`) * Create a symlink for this folder in your local Discourse instance "plugins" folder (eg. `path/to/your/discourse_folder/plugins/discourse_manager`)
* Make sure your Discourse instance is running locally at port 3000 and you are logged in as Admin * Make sure your Discourse instance is running locally at port 3000 and you are logged in as Admin
@ -14,9 +14,9 @@ This plugin works with the Discourse docker image. It allows you to perform upgr
* Install the client app dependencies: * Install the client app dependencies:
* `cd manager-client` * `cd manager-client`
* `npm install` * `yarn install`
* `bower install` * Make sure your local Discourse instance is running at port 3000
* Use `ember server --proxy "http://localhost:3000"` to proxy to your Discourse instance running on port 3000 * Run `./dev_server` which will run ember server for you with proxy to your local Discourse instance
* If that gives errors, you may need to start your Discourse rails server like this: `bundle exec rails s -b 127.0.0.1` * If that gives errors, you may need to start your Discourse rails server like this: `bundle exec rails s -b 127.0.0.1`
* JUST open up a browser to port 4200 and you're off to the races! * JUST open up a browser to port 4200 and you're off to the races!
@ -25,6 +25,15 @@ The client application is built using [Ember CLI](http://www.ember-cli.com/).
To create a compiled version for distribution, run `./compile_client.sh` to compile the site and To create a compiled version for distribution, run `./compile_client.sh` to compile the site and
move it into the proper directories. move it into the proper directories.
## Running tests
* Ruby
* Run `RAILS_ENV=test bundle exec rake plugin:spec[docker_manager]` in your discourse directory.
* JS Tests
* Run `ember s` in the `/manager-client` directory
* Open up your favorite browser and head to `http://localhost:4200/tests`and you should see all passing/failing tests
## Contributing ## Contributing
1. Fork it 1. Fork it

View File

@ -35,6 +35,7 @@ module DockerManager
if r.valid? if r.valid?
result[:id] = r.name.downcase.gsub(/[^a-z]/, '_').gsub(/_+/, '_').sub(/_$/, '') result[:id] = r.name.downcase.gsub(/[^a-z]/, '_').gsub(/_+/, '_').sub(/_$/, '')
result[:version] = r.latest_local_commit result[:version] = r.latest_local_commit
result[:pretty_version] = r.latest_local_tag_version.presence
result[:url] = r.url result[:url] = r.url
if r.upgrading? if r.upgrading?
result[:upgrading] = true result[:upgrading] = true
@ -61,6 +62,7 @@ module DockerManager
{ {
path: repo.path, path: repo.path,
version: repo.latest_origin_commit, version: repo.latest_origin_commit,
pretty_version: repo.latest_origin_tag_version.presence,
commits_behind: repo.commits_behind, commits_behind: repo.commits_behind,
date: repo.latest_origin_commit_date date: repo.latest_origin_commit_date
} }

View File

@ -7,25 +7,7 @@
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="manager-client/config/environment" content="%7B%22modulePrefix%22%3A%22manager-client%22%2C%22environment%22%3A%22development%22%2C%22rootURL%22%3A%22/%22%2C%22locationType%22%3A%22hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%7D%2C%22APP%22%3A%7B%22name%22%3A%22manager-client%22%2C%22version%22%3A%220.0.0+cfc32897%22%7D%2C%22exportApplicationGlobal%22%3Atrue%7D" /> <meta name="manager-client/config/environment" content="%7B%22modulePrefix%22%3A%22manager-client%22%2C%22environment%22%3A%22development%22%2C%22rootURL%22%3A%22/%22%2C%22locationType%22%3A%22hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%7D%2C%22APP%22%3A%7B%22name%22%3A%22manager-client%22%2C%22version%22%3A%220.0.0+cfc32897%22%7D%2C%22exportApplicationGlobal%22%3Atrue%7D" />
<meta id="preloaded-data" data-preload="<%= { rootUrl: discourse_root_url, longPollingBaseUrl: long_polling_base_url }.to_json %>">
<script>
window.Discourse = {
rootUrl: '<%= discourse_root_url %>',
longPollingBaseUrl: '<%= long_polling_base_url %>',
getURL: function(url) {
if (!url) return url;
// if it's a non relative URL, return it.
if (url !== '/' && !/^\/[^\/]/.test(url)) return url;
if (url.indexOf(Discourse.rootUrl) !== -1) return url;
if (url[0] !== "/") url = "/" + url;
return Discourse.rootUrl + url;
}
};
</script>
<%= javascript_include_tag "docker-manager-vendor" %> <%= javascript_include_tag "docker-manager-vendor" %>
<%= javascript_include_tag "docker-manager-app" %> <%= javascript_include_tag "docker-manager-app" %>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

0
dev_server.js → dev_server Executable file → Normal file
View File

View File

@ -37,6 +37,14 @@ class DockerManager::GitRepo
run "rev-parse --short #{tracking_branch}" run "rev-parse --short #{tracking_branch}"
end end
def latest_local_tag_version
prettify_tag_version("describe HEAD 2>/dev/null")
end
def latest_origin_tag_version
prettify_tag_version("describe #{tracking_branch} 2>/dev/null")
end
def latest_origin_commit_date def latest_origin_commit_date
commit_date(latest_origin_commit) commit_date(latest_origin_commit)
end end
@ -83,6 +91,16 @@ class DockerManager::GitRepo
protected protected
def prettify_tag_version(command)
result = run(command)
return unless result.present?
if result =~ /-(\d+)-/
result.gsub!(/-(\d+)-.*/, " +#{$1}")
end
result
end
def upgrade_key def upgrade_key
@upgrade_key ||= "upgrade:#{path}" @upgrade_key ||= "upgrade:#{path}"
end end

View File

@ -1,4 +0,0 @@
{
"directory": "bower_components",
"analytics": false
}

View File

@ -0,0 +1,20 @@
# unconventional js
/blueprints/*/files/
/vendor/
# compiled output
/dist/
/tmp/
# dependencies
/bower_components/
/node_modules/
# misc
/coverage/
!.*
# ember-try
/.node_modules.ember-try/
/bower.json.ember-try
/package.json.ember-try

View File

@ -0,0 +1,45 @@
module.exports = {
root: true,
parserOptions: {
ecmaVersion: 2017,
sourceType: 'module'
},
plugins: [
'ember'
],
globals: {
Em: false,
MessageBus: false
},
extends: [
'eslint:recommended',
'plugin:ember/recommended'
],
env: {
browser: true
},
rules: {
},
overrides: [
// node files
{
files: [
'.eslintrc.js',
'.template-lintrc.js',
'ember-cli-build.js',
'testem.js',
'blueprints/*/index.js',
'config/**/*.js',
'lib/*/index.js'
],
parserOptions: {
sourceType: 'script',
ecmaVersion: 2015
},
env: {
browser: false,
node: true
}
}
]
};

View File

@ -9,7 +9,7 @@ You will need the following things properly installed on your computer.
* [Git](https://git-scm.com/) * [Git](https://git-scm.com/)
* [Node.js](https://nodejs.org/) (with NPM) * [Node.js](https://nodejs.org/) (with NPM)
* [Bower](https://bower.io/) * [Yarn](https://yarnpkg.com)
* [Ember CLI](https://ember-cli.com/) * [Ember CLI](https://ember-cli.com/)
* [PhantomJS](http://phantomjs.org/) * [PhantomJS](http://phantomjs.org/)
@ -17,12 +17,11 @@ You will need the following things properly installed on your computer.
* `git clone <repository-url>` this repository * `git clone <repository-url>` this repository
* `cd manager-client` * `cd manager-client`
* `npm install` * `yarn install`
* `bower install`
## Running / Development ## Running / Development
* `ember serve` * `./dev_server` (in root directory of this plugin)
* Visit your app at [http://localhost:4200](http://localhost:4200). * Visit your app at [http://localhost:4200](http://localhost:4200).
### Code Generators ### Code Generators
@ -39,10 +38,6 @@ Make use of the many generators for code, try `ember help generate` for more det
* `ember build` (development) * `ember build` (development)
* `ember build --environment production` (production) * `ember build --environment production` (production)
### Deploying
Specify what it takes to deploy your app.
## Further Reading / Useful Links ## Further Reading / Useful Links
* [ember.js](http://emberjs.com/) * [ember.js](http://emberjs.com/)

View File

@ -1,13 +1,14 @@
import Ember from 'ember'; import Ember from "ember";
import Resolver from './resolver'; import Resolver from "./resolver";
import loadInitializers from 'ember-load-initializers'; import loadInitializers from "ember-load-initializers";
import config from './config/environment'; import config from "./config/environment";
import Application from "@ember/application";
let App; let App;
Ember.MODEL_FACTORY_INJECTIONS = true; Ember.MODEL_FACTORY_INJECTIONS = true;
App = Ember.Application.extend({ App = Application.extend({
modulePrefix: config.modulePrefix, modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix, podModulePrefix: config.podModulePrefix,
Resolver Resolver

View File

@ -1,21 +1,22 @@
import Ember from 'ember'; import Component from "@ember/component";
import { computed } from "@ember/object";
export default Ember.Component.extend({ export default Component.extend({
classNameBindings: [':progress', ':progress-striped', 'active'], classNameBindings: [":progress", ":progress-striped", "active"],
active: function() { active: computed("percent", function() {
return parseInt(this.get('percent'), 10) !== 100; return parseInt(this.get("percent"), 10) !== 100;
}.property('percent'), }),
barStyle: function() { barStyle: computed("percent", function() {
let percent = parseInt(this.get('percent'), 10); let percent = parseInt(this.get("percent"), 10);
if (percent > 0) { if (percent > 0) {
if (percent > 100) { percent = 100; } if (percent > 100) {
return ('width: ' + this.get('percent') + '%').htmlSafe(); percent = 100;
}
return ("width: " + this.get("percent") + "%").htmlSafe();
} }
return "".htmlSafe(); return "".htmlSafe();
}.property('percent') })
}); });

View File

@ -1,27 +1,43 @@
import Ember from 'ember'; import Discourse from "manager-client/discourse";
import Component from "@ember/component";
import { computed } from "@ember/object";
import { inject as service } from "@ember/service";
export default Ember.Component.extend({ export default Component.extend({
tagName: 'tr', router: service(),
tagName: "tr",
upgradeDisabled: function() { upgradeDisabled: computed(
const upgradingRepo = this.get('upgradingRepo'); "upgradingRepo",
"repo",
"managerRepo",
"managerRepo.upToDate",
function() {
const upgradingRepo = this.get("upgradingRepo");
if (!upgradingRepo) { if (!upgradingRepo) {
const managerRepo = this.get('managerRepo'); const managerRepo = this.get("managerRepo");
if (!managerRepo) { return false; } if (!managerRepo) {
return (!managerRepo.get('upToDate')) && managerRepo !== this.get('repo'); return false;
}
return !managerRepo.get("upToDate") && managerRepo !== this.get("repo");
}
return true;
} }
return true; ),
}.property('upgradingRepo', 'repo', 'managerRepo', 'managerRepo.upToDate'),
officialRepoImageSrc: function() { officialRepoImageSrc: computed("repo.official", function() {
if (!this.get('repo.official')) { return; } if (!this.get("repo.official")) {
return Discourse.getURL("/plugins/docker_manager/images/font-awesome-check-circle.png"); return;
}.property('repo.official'), }
return Discourse.getURL(
"/plugins/docker_manager/images/font-awesome-check-circle.png"
);
}),
actions: { actions: {
upgrade: function() { upgrade() {
this.sendAction('upgrade', this.get('repo')); this.get("router").transitionTo("upgrade", this.get("repo"));
} }
} }
}); });

View File

@ -1,20 +1,22 @@
import Ember from 'ember'; import Component from "@ember/component";
import { observer } from "@ember/object";
import { scheduleOnce } from "@ember/runloop";
export default Ember.Component.extend({ export default Component.extend({
classNameBindings: [':logs'], classNameBindings: [":logs"],
_outputChanged: function() { _outputChanged: observer("output", function() {
Ember.run.scheduleOnce('afterRender', this, '_scrollBottom'); scheduleOnce("afterRender", this, "_scrollBottom");
}.observes('output'), }),
_scrollBottom: function() { _scrollBottom() {
if (this.get('followOutput')) { if (this.get("followOutput")) {
this.$().scrollTop(this.$()[0].scrollHeight); this.$().scrollTop(this.$()[0].scrollHeight);
} }
}, },
_scrollOnInsert: function() { didInsertElement() {
this._super(...arguments);
this._scrollBottom(); this._scrollBottom();
}.on('didInsertElement') }
}); });

View File

@ -1,39 +1,41 @@
/* global Discourse */ import Discourse from "manager-client/discourse";
import Controller from "@ember/controller";
import { computed } from "@ember/object";
import Ember from "ember"; export default Controller.extend({
showBanner: computed("banner", "bannerDismissed", "banner.[]", function() {
export default Ember.Controller.extend({ if (this.get("bannerDismissed")) {
showBanner: function(){
if(this.get("bannerDismissed")){
return false; return false;
} }
const banner = this.get("banner"); const banner = this.get("banner");
return banner && banner.length > 0; return banner && banner.length > 0;
}.property("banner", "bannerDismissed", "banner.@each"), }),
appendBannerHtml: function(html){ appendBannerHtml(html) {
const banner = this.get("banner") || []; const banner = this.get("banner") || [];
if(banner.indexOf(html) === -1){ if (banner.indexOf(html) === -1) {
banner.pushObject(html); banner.pushObject(html);
} }
this.set("banner", banner); this.set("banner", banner);
}, },
logoUrl: function() { logoUrl: computed(function() {
return Discourse.getURL("/assets/images/docker-manager-aff8eaea0445c0488c19f8cfd14faa8c2b278924438f19048eacc175d7d134e4.png"); return Discourse.getURL(
}.property(), "/assets/images/docker-manager-aff8eaea0445c0488c19f8cfd14faa8c2b278924438f19048eacc175d7d134e4.png"
);
}),
returnToSiteUrl: function() { returnToSiteUrl: computed(function() {
return Discourse.getURL("/"); return Discourse.getURL("/");
}.property(), }),
backupsUrl: function() { backupsUrl: computed(function() {
return Discourse.getURL("/admin/backups"); return Discourse.getURL("/admin/backups");
}.property(), }),
actions: { actions: {
dismiss: function () { dismiss() {
this.set("bannerDismissed", true); this.set("bannerDismissed", true);
} }
} }

View File

@ -1,16 +1,21 @@
import Ember from 'ember'; import Controller from "@ember/controller";
import { computed } from "@ember/object";
export default Ember.Controller.extend({ export default Controller.extend({
managerRepo: null, managerRepo: null,
upgrading: null, upgrading: null,
upgradeAllButtonDisabled: function () { upgradeAllButtonDisabled: computed(
return !this.get("managerRepo.upToDate") || this.get("allUpToDate"); "managerRepo.upToDate",
}.property("managerRepo.upToDate", "allUpToDate"), "allUpToDate",
function() {
return !this.get("managerRepo.upToDate") || this.get("allUpToDate");
}
),
allUpToDate: function() { allUpToDate: computed("model.[].upToDate", function() {
return this.get("model").every(repo => repo.get("upToDate")); return this.get("model").every(repo => repo.get("upToDate"));
}.property("model.@each.upToDate"), }),
actions: { actions: {
upgradeAllButton() { upgradeAllButton() {

View File

@ -1,22 +1,18 @@
import Ember from 'ember'; import Controller from "@ember/controller";
export default Ember.Controller.extend({ export default Controller.extend({
autoRefresh: false, autoRefresh: false,
init: function() { init() {
this._super(); this._super();
var self = this; window.setInterval(() => {
this.performRefresh();
window.setInterval(function() {
self.performRefresh();
}, 5000); }, 5000);
}, },
performRefresh: function() { performRefresh() {
if (this.get('autoRefresh')) { if (this.get("autoRefresh")) {
this.get('model').refresh(); this.get("model").refresh();
} }
} }
}); });

View File

@ -1,8 +1,10 @@
/* global MessageBus, bootbox */ /* global MessageBus, bootbox */
import Ember from 'ember'; import Repo from "manager-client/models/repo";
import Repo from 'manager-client/models/repo'; import Controller from "@ember/controller";
import { equal } from "@ember/object/computed";
import { computed } from "@ember/object";
export default Ember.Controller.extend({ export default Controller.extend({
output: null, output: null,
init() { init() {
@ -10,24 +12,24 @@ export default Ember.Controller.extend({
this.reset(); this.reset();
}, },
complete: Ember.computed.equal('status', 'complete'), complete: equal("status", "complete"),
failed: Ember.computed.equal('status', 'failed'), failed: equal("status", "failed"),
multiUpgrade: function() { multiUpgrade: computed("model.length", function() {
return this.get("model.length") !== 1; return this.get("model.length") !== 1;
}.property("model.length"), }),
title: function () { title: computed("model.[].name", function() {
return this.get("multiUpgrade") ? "All" : this.get("model")[0].get("name"); return this.get("multiUpgrade") ? "All" : this.get("model")[0].get("name");
}.property("model.@each.name"), }),
isUpToDate: function () { isUpToDate: computed("model.[].upToDate", function() {
return this.get("model").every(repo => repo.get("upToDate")); return this.get("model").every(repo => repo.get("upToDate"));
}.property("model.@each.upToDate"), }),
upgrading: function () { upgrading: computed("model.[].upgrading", function() {
return this.get("model").some(repo => repo.get("upgrading")); return this.get("model").some(repo => repo.get("upgrading"));
}.property("model.@each.upgrading"), }),
repos() { repos() {
const model = this.get("model"); const model = this.get("model");
@ -36,43 +38,45 @@ export default Ember.Controller.extend({
updateAttribute(key, value, valueIsKey = false) { updateAttribute(key, value, valueIsKey = false) {
this.get("model").forEach(repo => { this.get("model").forEach(repo => {
value = valueIsKey ? repo.get(value) : value; value = valueIsKey ? repo.get(value) : value;
repo.set(key, value); repo.set(key, value);
}); });
}, },
messageReceived(msg) { messageReceived(msg) {
switch(msg.type) { switch (msg.type) {
case "log": case "log":
this.set('output', this.get('output') + msg.value + "\n"); this.set("output", this.get("output") + msg.value + "\n");
break; break;
case "percent": case "percent":
this.set('percent', msg.value); this.set("percent", msg.value);
break; break;
case "status": case "status":
this.set('status', msg.value); this.set("status", msg.value);
if (msg.value === "complete") { if (msg.value === "complete") {
this.get("model").filter(repo => repo.get("upgrading")).forEach(repo => { this.get("model")
repo.set("version", repo.get("latest.version")); .filter(repo => repo.get("upgrading"))
}); .forEach(repo => {
repo.set("version", repo.get("latest.version"));
});
} }
if (msg.value === 'complete' || msg.value === 'failed') { if (msg.value === "complete" || msg.value === "failed") {
this.updateAttribute('upgrading', false); this.updateAttribute("upgrading", false);
} }
break; break;
} }
}, },
upgradeButtonText: function() { upgradeButtonText: computed("upgrading", function() {
if (this.get("upgrading")) { if (this.get("upgrading")) {
return "Upgrading..."; return "Upgrading...";
} else { } else {
return "Start Upgrading"; return "Start Upgrading";
} }
}.property("upgrading"), }),
startBus() { startBus() {
MessageBus.subscribe("/docker/upgrade", msg => { MessageBus.subscribe("/docker/upgrade", msg => {
@ -85,7 +89,7 @@ export default Ember.Controller.extend({
}, },
reset() { reset() {
this.setProperties({ output: '', status: null, percent: 0 }); this.setProperties({ output: "", status: null, percent: 0 });
}, },
actions: { actions: {
@ -93,33 +97,41 @@ export default Ember.Controller.extend({
this.reset(); this.reset();
if (this.get("multiUpgrade")) { if (this.get("multiUpgrade")) {
this.get("model").filter(repo => !repo.get("upToDate")).forEach(repo => repo.set("upgrading", true)); this.get("model")
.filter(repo => !repo.get("upToDate"))
.forEach(repo => repo.set("upgrading", true));
return Repo.upgradeAll(); return Repo.upgradeAll();
} }
const repo = this.get('model')[0]; const repo = this.get("model")[0];
if (repo.get('upgrading')) { return; } if (repo.get("upgrading")) {
return;
}
repo.startUpgrade(); repo.startUpgrade();
}, },
resetUpgrade() { resetUpgrade() {
bootbox.confirm("WARNING: You should only reset upgrades that have failed and are not running.\n\n"+ bootbox.confirm(
"This will NOT cancel currently running builds and should only be used as a last resort.", result => { "WARNING: You should only reset upgrades that have failed and are not running.\n\n" +
if (result) { "This will NOT cancel currently running builds and should only be used as a last resort.",
if (this.get("multiUpgrade")) { result => {
return Repo.resetAll(this.get("model").filter(repo => !repo.get("upToDate"))).finally(() => { if (result) {
if (this.get("multiUpgrade")) {
return Repo.resetAll(
this.get("model").filter(repo => !repo.get("upToDate"))
).finally(() => {
this.reset();
this.updateAttribute("upgrading", false);
});
}
const repo = this.get("model")[0];
repo.resetUpgrade().then(() => {
this.reset(); this.reset();
this.updateAttribute("upgrading", false);
}); });
} }
const repo = this.get('model')[0];
repo.resetUpgrade().then(function() {
this.reset();
});
} }
}); );
} }
}, }
}); });

View File

@ -0,0 +1,23 @@
function init() {
const data = Em.$("#preloaded-data").data("preload");
Em.$.extend(Discourse, data);
}
const Discourse = {
getURL(url) {
if (!this.hasOwnProperty("rootUrl")) {
init();
}
if (!url) return url;
// if it's a non relative URL, return it.
if (url !== "/" && !/^\/[^/]/.test(url)) return url;
if (url.indexOf(this.rootUrl) !== -1) return url;
if (url[0] !== "/") url = "/" + url;
return this.rootUrl + url;
}
};
export default Discourse;

View File

@ -1,6 +1,8 @@
import Ember from "ember"; import { helper as buildHelper } from "@ember/component/helper";
import { isNone } from "@ember/utils";
import { htmlSafe } from "@ember/template";
export default Ember.Helper.helper(function(params) { export default buildHelper(function(params) {
const [commitsBehind, oldSha, newSha, url] = params; const [commitsBehind, oldSha, newSha, url] = params;
if (parseInt(commitsBehind) === 0) { if (parseInt(commitsBehind) === 0) {
@ -11,12 +13,12 @@ export default Ember.Helper.helper(function(params) {
commitsBehind === 1 ? "" : "s" commitsBehind === 1 ? "" : "s"
}`; }`;
if (Ember.isNone(url)) { if (isNone(url)) {
return description; return description;
} }
var _url = url.substr(0, url.search(/(\.git)?$/)); const _url = url.substr(0, url.search(/(\.git)?$/));
description = `<a href='${_url}/compare/${oldSha}..${newSha}'>${description}</a>`; description = `<a href='${_url}/compare/${oldSha}...${newSha}'>${description}</a>`;
return new Ember.String.htmlSafe(description); return new htmlSafe(description);
}); });

View File

@ -6,6 +6,7 @@
<title>DockerManager</title> <title>DockerManager</title>
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta id="preloaded-data" data-preload="{&quot;rootUrl&quot;:&quot;/&quot;,&quot;longPollingBaseUrl&quot;:&quot;/&quot;}">
{{content-for 'head'}} {{content-for 'head'}}
@ -14,17 +15,6 @@
{{content-for 'head-footer'}} {{content-for 'head-footer'}}
<script>
window.Discourse = {
rootUrl: '/',
longPollingBaseUrl: '/',
getURL: function(url) {
return url;
}
};
</script>
</head> </head>
<body> <body>
{{content-for 'body'}} {{content-for 'body'}}

View File

@ -1,15 +1,14 @@
/* global $:true, Discourse */ import Discourse from "manager-client/discourse";
import request from 'ember-ajax/request';
export default { export default {
name: "findCsrfToken", name: "findCsrfToken",
initialize() { initialize() {
return request(Discourse.getURL('/session/csrf')).then(function(result) { return Em.$.ajax(Discourse.getURL("/session/csrf")).then(result => {
var token = result.csrf; const token = result.csrf;
$.ajaxPrefilter(function(options, originalOptions, xhr) { Em.$.ajaxPrefilter((options, originalOptions, xhr) => {
if (!options.crossDomain) { if (!options.crossDomain) {
xhr.setRequestHeader('X-CSRF-Token', token); xhr.setRequestHeader("X-CSRF-Token", token);
} }
}); });
}); });

View File

@ -1,4 +1,4 @@
/* global MessageBus, Discourse, $:true */ import Discourse from "manager-client/discourse";
export default { export default {
name: "message-bus", name: "message-bus",
@ -6,14 +6,16 @@ export default {
initialize() { initialize() {
MessageBus.baseUrl = Discourse.longPollingBaseUrl; MessageBus.baseUrl = Discourse.longPollingBaseUrl;
if (MessageBus.baseUrl !== '/') { if (MessageBus.baseUrl !== "/") {
MessageBus.ajax = function(opts) { MessageBus.ajax = function(opts) {
opts.headers = opts.headers || {}; opts.headers = opts.headers || {};
opts.headers['X-Shared-Session-Key'] = $('meta[name=shared_session_key]').attr('content'); opts.headers["X-Shared-Session-Key"] = Em.$(
return $.ajax(opts); "meta[name=shared_session_key]"
).attr("content");
return Em.$.ajax(opts);
}; };
} else { } else {
MessageBus.baseUrl = Discourse.getURL('/'); MessageBus.baseUrl = Discourse.getURL("/");
} }
} }
}; };

View File

@ -1,14 +1,14 @@
/* global Discourse */ import EmberObject from "@ember/object";
import Discourse from "manager-client/discourse";
import request from 'ember-ajax/request'; const ProcessList = EmberObject.extend({
import Ember from 'ember';
const ProcessList = Ember.Object.extend({
output: null, output: null,
refresh() { refresh() {
return request(Discourse.getURL("/admin/docker/ps"), {dataType: 'text'}).then(result => { return Em.$.ajax(Discourse.getURL("/admin/docker/ps"), {
this.set('output', result); dataType: "text"
}).then(result => {
this.set("output", result);
return this; return this;
}); });
} }

View File

@ -1,7 +1,8 @@
/* global Discourse */ import Discourse from "manager-client/discourse";
import { default as EmberObject, computed } from "@ember/object";
import request from 'ember-ajax/request'; import { or } from "@ember/object/computed";
import Ember from 'ember'; import { isNone } from "@ember/utils";
import { Promise } from "rsvp";
let loaded = []; let loaded = [];
@ -9,109 +10,150 @@ function concatVersions(repos) {
return repos.map(repo => repo.get("version")).join(", "); return repos.map(repo => repo.get("version")).join(", ");
} }
const Repo = Ember.Object.extend({ const Repo = EmberObject.extend({
unloaded: true, unloaded: true,
checking: false, checking: false,
checkingStatus: Ember.computed.or('unloaded', 'checking'), checkingStatus: or("unloaded", "checking"),
upToDate: function() { upToDate: computed("upgrading", "version", "latest.version", function() {
return !this.get('upgrading') & (this.get('version') === this.get('latest.version')); return (
}.property('upgrading', 'version', 'latest.version'), !this.get("upgrading") &
(this.get("version") === this.get("latest.version"))
);
}),
shouldCheck: function() { prettyVersion: computed("version", "pretty_version", function() {
if (Ember.isNone(this.get('version'))) { return false; } return this.get("pretty_version") || this.get("version");
if (this.get('checking')) { return false; } }),
prettyLatestVersion: computed("latest.{version,pretty_version}", function() {
return this.get("latest.pretty_version") || this.get("latest.version");
}),
shouldCheck: computed(function() {
if (isNone(this.get("version"))) {
return false;
}
if (this.get("checking")) {
return false;
}
// Only check once every minute // Only check once every minute
var lastCheckedAt = this.get('lastCheckedAt'); const lastCheckedAt = this.get("lastCheckedAt");
if (lastCheckedAt) { if (lastCheckedAt) {
var ago = new Date().getTime() - lastCheckedAt; const ago = new Date().getTime() - lastCheckedAt;
return ago > 60 * 1000; return ago > 60 * 1000;
} }
return true; return true;
}.property().volatile(), }).volatile(),
repoAjax: function(url, args) { repoAjax(url, args) {
args = args || {}; args = args || {};
args.data = this.getProperties('path', 'version', 'branch'); args.data = this.getProperties("path", "version", "branch");
return request(Discourse.getURL(url), args); return Em.$.ajax(Discourse.getURL(url), args);
}, },
findLatest: function() { findLatest() {
return new Ember.RSVP.Promise(resolve => { return new Promise(resolve => {
if (!this.get('shouldCheck')) { if (!this.get("shouldCheck")) {
this.set('unloaded', false); this.set("unloaded", false);
return resolve(); return resolve();
} }
this.set('checking', true); this.set("checking", true);
this.repoAjax(Discourse.getURL('/admin/docker/latest')).then(result => { this.repoAjax(Discourse.getURL("/admin/docker/latest")).then(result => {
this.setProperties({ this.setProperties({
unloaded: false, unloaded: false,
checking: false, checking: false,
lastCheckedAt: new Date().getTime(), lastCheckedAt: new Date().getTime(),
latest: Ember.Object.create(result.latest) latest: EmberObject.create(result.latest)
}); });
resolve(); resolve();
}); });
}); });
}, },
findProgress: function() { findProgress() {
return this.repoAjax(Discourse.getURL('/admin/docker/progress')).then(result => result.progress); return this.repoAjax(Discourse.getURL("/admin/docker/progress")).then(
result => result.progress
);
}, },
resetUpgrade: function() { resetUpgrade() {
return this.repoAjax(Discourse.getURL('/admin/docker/upgrade'), { dataType: 'text', type: 'DELETE' }).then(() => { return this.repoAjax(Discourse.getURL("/admin/docker/upgrade"), {
this.set('upgrading', false); dataType: "text",
type: "DELETE"
}).then(() => {
this.set("upgrading", false);
}); });
}, },
startUpgrade: function() { startUpgrade() {
this.set('upgrading', true); this.set("upgrading", true);
return this.repoAjax(Discourse.getURL('/admin/docker/upgrade'), { dataType: 'text', type: 'POST' }).catch(() => { return this.repoAjax(Discourse.getURL("/admin/docker/upgrade"), {
this.set('upgrading', false); dataType: "text",
type: "POST"
}).catch(() => {
this.set("upgrading", false);
}); });
} }
}); });
Repo.reopenClass({ Repo.reopenClass({
findAll() { findAll() {
return new Ember.RSVP.Promise(function (resolve) { return new Promise(resolve => {
if (loaded.length) { return resolve(loaded); } if (loaded.length) {
return resolve(loaded);
}
request(Discourse.getURL("/admin/docker/repos")).then(result => { Em.$.ajax(Discourse.getURL("/admin/docker/repos")).then(result => {
loaded = result.repos.map(r => Repo.create(r)); loaded = result.repos.map(r => Repo.create(r));
resolve(loaded); resolve(loaded);
}); });
}); });
}, },
findUpgrading: function() { findUpgrading() {
return this.findAll().then(result => result.findBy('upgrading', true)); return this.findAll().then(result => result.findBy("upgrading", true));
}, },
find: function(id) { find(id) {
return this.findAll().then(result => result.findBy('id', id)); return this.findAll().then(result => result.findBy("id", id));
}, },
upgradeAll() { upgradeAll() {
return request(Discourse.getURL("/admin/docker/upgrade"), { dataType: "text", type: "POST", data: { path: "all" } }); return Em.$.ajax(Discourse.getURL("/admin/docker/upgrade"), {
dataType: "text",
type: "POST",
data: { path: "all" }
});
}, },
resetAll(repos) { resetAll(repos) {
return request(Discourse.getURL("/admin/docker/upgrade"), { dataType: "text", type: "DELETE", data: { path: "all", version: concatVersions(repos) } }); return Em.$.ajax(Discourse.getURL("/admin/docker/upgrade"), {
dataType: "text",
type: "DELETE",
data: { path: "all", version: concatVersions(repos) }
});
}, },
findLatestAll() { findLatestAll() {
return request(Discourse.getURL("/admin/docker/latest"), { dataType: "text", type: "GET", data: { path: "all" } }); return Em.$.ajax(Discourse.getURL("/admin/docker/latest"), {
dataType: "text",
type: "GET",
data: { path: "all" }
});
}, },
findAllProgress(repos) { findAllProgress(repos) {
return request(Discourse.getURL("/admin/docker/progress"), { dataType: "text", type: "GET", data: { path: "all", version: concatVersions(repos) } }); return Em.$.ajax(Discourse.getURL("/admin/docker/progress"), {
}, dataType: "text",
type: "GET",
data: { path: "all", version: concatVersions(repos) }
});
}
}); });
export default Repo; export default Repo;

View File

@ -1,3 +1,3 @@
import Resolver from 'ember-resolver'; import Resolver from "ember-resolver";
export default Resolver; export default Resolver;

View File

@ -1,14 +1,14 @@
import Ember from 'ember'; import config from "./config/environment";
import config from './config/environment'; import EmberRouter from "@ember/routing/router";
const Router = Ember.Router.extend({ const Router = EmberRouter.extend({
location: config.locationType, location: config.locationType,
rootURL: config.rootURL rootURL: config.rootURL
}); });
Router.map(function() { Router.map(function() {
this.route("processes"); this.route("processes");
this.route('upgrade', { path: '/upgrade/:id' }); this.route("upgrade", { path: "/upgrade/:id" });
}); });
export default Router; export default Router;

View File

@ -1,13 +1,15 @@
import Repo from 'manager-client/models/repo'; import Repo from "manager-client/models/repo";
import Ember from 'ember'; import Route from "@ember/routing/route";
export default Ember.Route.extend({ export default Route.extend({
model() { model() {
return Repo.findAll(); return Repo.findAll();
}, },
loadRepos(list) { loadRepos(list) {
if (list.length === 0) { return; } if (list.length === 0) {
return;
}
this.loadRepo(list.shift()).then(() => this.loadRepos(list)); this.loadRepo(list.shift()).then(() => this.loadRepos(list));
}, },
@ -16,31 +18,30 @@ export default Ember.Route.extend({
}, },
setupController(controller, model) { setupController(controller, model) {
const applicationController = this.controllerFor('application'); const applicationController = this.controllerFor("application");
controller.setProperties({ model, upgrading: null }); controller.setProperties({ model, upgrading: null });
model.forEach(repo => { model.forEach(repo => {
if (repo.get('upgrading')) { if (repo.get("upgrading")) {
controller.set('upgrading', repo); controller.set("upgrading", repo);
} }
// Special case: Upgrade docker manager first // Special case: Upgrade docker manager first
if (repo.get('id') === 'docker_manager') { if (repo.get("id") === "docker_manager") {
controller.set('managerRepo', repo); controller.set("managerRepo", repo);
} }
// Special case: If the branch is "master" warn user // Special case: If the branch is "master" warn user
if (repo.get('id') === 'discourse' && repo.get('branch') === 'origin/master') { if (
applicationController.appendBannerHtml("<b>WARNING:</b> Your Discourse is tracking the 'master' branch which may be unstable, <a href='https://meta.discourse.org/t/change-tracking-branch-for-your-discourse-instance/17014'>we recommend tracking the 'tests-passed' branch</a>."); repo.get("id") === "discourse" &&
repo.get("branch") === "origin/master"
) {
applicationController.appendBannerHtml(
"<b>WARNING:</b> Your Discourse is tracking the 'master' branch which may be unstable, <a href='https://meta.discourse.org/t/change-tracking-branch-for-your-discourse-instance/17014'>we recommend tracking the 'tests-passed' branch</a>."
);
} }
}); });
this.loadRepos(model.slice(0)); this.loadRepos(model.slice(0));
},
actions: {
upgrade(repo) {
this.transitionTo('upgrade', repo);
}
} }
}); });

View File

@ -1,6 +1,6 @@
import { find } from 'manager-client/models/process-list'; import { find } from "manager-client/models/process-list";
import Ember from 'ember'; import Route from "@ember/routing/route";
export default Ember.Route.extend({ export default Route.extend({
model: find model: find
}); });

View File

@ -1,26 +1,31 @@
import Repo from 'manager-client/models/repo'; import Repo from "manager-client/models/repo";
import Ember from 'ember'; import Route from "@ember/routing/route";
import EmberObject from "@ember/object";
import { Promise } from "rsvp";
export default Ember.Route.extend({ export default Route.extend({
model(params) {
model: function(params) {
if (params.id === "all") { if (params.id === "all") {
return Repo.findAll(); return Repo.findAll();
} }
return Repo.find(params.id); return Repo.find(params.id);
}, },
afterModel: function(model) { afterModel(model) {
if (Array.isArray(model)) { if (Array.isArray(model)) {
return Repo.findLatestAll().then(response => { return Repo.findLatestAll().then(response => {
JSON.parse(response).repos.forEach(_repo => { JSON.parse(response).repos.forEach(_repo => {
const repo = model.find(repo => repo.get("path") === _repo.path); const repo = model.find(repo => repo.get("path") === _repo.path);
if (!repo) { return; } if (!repo) {
return;
}
delete _repo.path; delete _repo.path;
repo.set("latest", Ember.Object.create(_repo)); repo.set("latest", EmberObject.create(_repo));
}); });
return Repo.findAllProgress(model.filter(repo => !repo.get("upToDate"))).then(progress => { return Repo.findAllProgress(
model.filter(repo => !repo.get("upToDate"))
).then(progress => {
this.set("progress", JSON.parse(progress).progress); this.set("progress", JSON.parse(progress).progress);
}); });
}); });
@ -28,7 +33,7 @@ export default Ember.Route.extend({
return Repo.findUpgrading().then(u => { return Repo.findUpgrading().then(u => {
if (u && u !== model) { if (u && u !== model) {
return Ember.RSVP.Promise.reject("wat"); return Promise.reject("wat");
} }
return model.findLatest().then(() => { return model.findLatest().then(() => {
return model.findProgress().then(progress => { return model.findProgress().then(progress => {
@ -36,21 +41,19 @@ export default Ember.Route.extend({
}); });
}); });
}); });
}, },
setupController: function(controller, model) { setupController(controller, model) {
controller.reset(); controller.reset();
controller.setProperties({ controller.setProperties({
model: Array.isArray(model) ? model : [model], model: Array.isArray(model) ? model : [model],
output: this.get('progress.logs'), output: this.get("progress.logs"),
percent: this.get('progress.percentage') percent: this.get("progress.percentage")
}); });
controller.startBus(); controller.startBus();
}, },
deactivate: function() { deactivate() {
this.controllerFor('upgrade').stopBus(); this.controllerFor("upgrade").stopBus();
} }
}); });

View File

@ -113,6 +113,10 @@ h3.loading {
} }
} }
span.commit-hash {
color: #959595;
}
button.btn { button.btn {
background: #00aaff; background: #00aaff;
color: white !important; color: white !important;

View File

@ -1,11 +1,11 @@
<td> <td>
{{#if repo.official}} {{#if repo.official}}
<img class="check-circle" src={{officialRepoImageSrc}} alt="Official Plugin" title="Official Plugin"/> <img class="check-circle" src={{officialRepoImageSrc}} alt="Official Plugin" title="Official Plugin">
{{/if}} {{/if}}
</td> </td>
<td> <td>
{{repo.name}} <a href="{{repo.url}}">{{repo.name}}</a>
{{repo.version}} <span class="current commit-hash">{{repo.prettyVersion}}</span>
</td> </td>
<td> <td>
{{#if repo.checkingStatus}} {{#if repo.checkingStatus}}
@ -16,7 +16,7 @@
<div class='new-version'> <div class='new-version'>
<h4>New Version Available!</h4> <h4>New Version Available!</h4>
<ul> <ul>
<li>Remote Version: {{repo.latest.version}}</li> <li>Remote Version: <span class="new commit-hash">{{repo.prettyLatestVersion}}</span></li>
<li>Last Updated: <li>Last Updated:
{{#if repo.latest.date}} {{#if repo.latest.date}}
{{moment-from-now repo.latest.date interval=1000}} {{moment-from-now repo.latest.date interval=1000}}
@ -27,9 +27,9 @@
<li class='new-commits'>{{new-commits repo.latest.commits_behind repo.version repo.latest.version repo.url}}</li> <li class='new-commits'>{{new-commits repo.latest.commits_behind repo.version repo.latest.version repo.url}}</li>
</ul> </ul>
{{#if repo.upgrading}} {{#if repo.upgrading}}
<button class="btn" {{action "upgrade"}}>Currently Upgrading...</button> <button class="btn" disabled=true>Currently Upgrading...</button>
{{else}} {{else}}
<button class="btn" {{action "upgrade"}} disabled={{upgradeDisabled}}>Upgrade</button> <button class="upgrade-button btn" {{action "upgrade"}} disabled={{upgradeDisabled}}>Upgrade</button>
{{/if}} {{/if}}
</div> </div>
{{/if}} {{/if}}

View File

@ -14,7 +14,7 @@
</tr> </tr>
<tbody> <tbody>
{{#each model as |repo|}} {{#each model as |repo|}}
{{repo-status repo=repo upgradingRepo=upgrading managerRepo=managerRepo upgrade="upgrade"}} {{repo-status repo=repo upgradingRepo=upgrading managerRepo=managerRepo}}
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>

View File

@ -1,9 +0,0 @@
{
"name": "manager-client",
"dependencies": {
"ember": "~2.10.0",
"bootbox": "~4.3.0",
"ember-cli-shims": "0.1.3",
"message-bus": "https://github.com/SamSaffron/message_bus.git#84f733c14e5b4e7f2464b5b45944ceccec727899"
}
}

View File

@ -40,6 +40,7 @@ module.exports = function(environment) {
ENV.APP.LOG_VIEW_LOOKUPS = false; ENV.APP.LOG_VIEW_LOOKUPS = false;
ENV.APP.rootElement = '#ember-testing'; ENV.APP.rootElement = '#ember-testing';
ENV.APP.autoboot = false;
} }
if (environment === 'production') { if (environment === 'production') {

View File

@ -23,10 +23,10 @@ module.exports = function(defaults) {
// modules that you would like to import into your application // modules that you would like to import into your application
// please specify an object with the list of modules as keys // please specify an object with the list of modules as keys
// along with the exports of each module as its value. // along with the exports of each module as its value.
app.import("bower_components/bootbox/bootbox.js"); app.import("node_modules/bootbox/bootbox.min.js");
app.import("bower_components/message-bus/assets/message-bus.js"); app.import("node_modules/bootstrap/js/dist/util.js");
app.import("bower_components/bootstrap/js/dist/util.js"); app.import("node_modules/bootstrap/js/dist/modal.js");
app.import("bower_components/bootstrap/js/dist/modal.js"); app.import("vendor/message-bus.js");
return app.toTree(); return app.toTree();
}; };

File diff suppressed because it is too large Load Diff

View File

@ -15,31 +15,43 @@
"test": "ember test" "test": "ember test"
}, },
"devDependencies": { "devDependencies": {
"broccoli-asset-rev": "^2.4.5", "@ember/jquery": "^0.5.2",
"ember-ajax": "2.5.3", "@ember/optional-features": "^0.6.3",
"ember-cli": "2.10.0", "broccoli-asset-rev": "^2.7.0",
"ember-cli-app-version": "^2.0.0", "ember-ajax": "^4.0.1",
"ember-cli-babel": "^5.1.7", "ember-cli": "~3.6.0",
"ember-cli-dependency-checker": "^1.3.0", "ember-cli-app-version": "^3.2.0",
"ember-cli-htmlbars": "^1.0.10", "ember-cli-babel": "^7.1.2",
"ember-cli-htmlbars-inline-precompile": "^0.3.3", "ember-cli-dependency-checker": "^3.0.0",
"ember-cli-inject-live-reload": "^1.4.1", "ember-cli-eslint": "^4.2.3",
"ember-cli-jshint": "^2.0.1", "ember-cli-htmlbars": "^3.0.0",
"ember-cli-moment-shim": "3.0.1", "ember-cli-htmlbars-inline-precompile": "^1.0.3",
"ember-cli-qunit": "^3.0.1", "ember-cli-inject-live-reload": "^1.8.2",
"ember-cli-release": "^0.2.9", "ember-cli-moment-shim": "3.7.1",
"ember-cli-sass": "6.1.0", "ember-cli-sass": "8.0.1",
"ember-cli-sri": "^2.1.0", "ember-cli-sri": "^2.1.1",
"ember-cli-test-loader": "^1.1.0", "ember-cli-template-lint": "^1.0.0-beta.1",
"ember-cli-uglify": "^1.2.0", "ember-cli-uglify": "^2.1.0",
"ember-export-application-global": "^1.0.5", "ember-data": "~3.6.0",
"ember-load-initializers": "^0.5.1", "ember-export-application-global": "^2.0.0",
"ember-moment": "7.3.0", "ember-load-initializers": "^1.1.0",
"ember-resolver": "^2.0.3", "ember-maybe-import-regenerator": "^0.1.6",
"loader.js": "^4.0.10" "ember-moment": "7.8.0",
"ember-qunit": "^3.4.1",
"ember-resolver": "^5.0.1",
"ember-source": "~3.6.0",
"ember-welcome-page": "^3.2.0",
"eslint-plugin-ember": "^5.2.0",
"loader.js": "^4.7.0",
"qunit-dom": "^0.8.0",
"sass": "^1.15.2"
}, },
"engines": { "engines": {
"node": ">= 0.12.0" "node": ">= 0.12.0"
}, },
"private": true "private": true,
"dependencies": {
"bootbox": "^4.4.0",
"bootstrap": "^4.2.1"
}
} }

View File

@ -1,5 +1,5 @@
import Ember from 'ember'; import { run } from "@ember/runloop";
export default function destroyApp(application) { export default function destroyApp(application) {
Ember.run(application, 'destroy'); run(application, "destroy");
} }

View File

@ -1,9 +1,7 @@
import { module } from 'qunit'; import { module } from "qunit";
import Ember from 'ember'; import startApp from "../helpers/start-app";
import startApp from '../helpers/start-app'; import destroyApp from "../helpers/destroy-app";
import destroyApp from '../helpers/destroy-app'; import { Promise } from "rsvp";
const { RSVP: { Promise } } = Ember;
export default function(name, options = {}) { export default function(name, options = {}) {
module(name, { module(name, {
@ -16,8 +14,11 @@ export default function(name, options = {}) {
}, },
afterEach() { afterEach() {
let afterEach = options.afterEach && options.afterEach.apply(this, arguments); let afterEach =
return Promise.resolve(afterEach).then(() => destroyApp(this.application)); options.afterEach && options.afterEach.apply(this, arguments);
return Promise.resolve(afterEach).then(() =>
destroyApp(this.application)
);
} }
}); });
} }

View File

@ -1,5 +1,5 @@
import Resolver from '../../resolver'; import Resolver from "../../resolver";
import config from '../../config/environment'; import config from "../../config/environment";
const resolver = Resolver.create(); const resolver = Resolver.create();

View File

@ -1,14 +1,15 @@
import Ember from 'ember'; import Application from "../../app";
import Application from '../../app'; import config from "../../config/environment";
import config from '../../config/environment'; import { assign } from "@ember/polyfills";
import { run } from "@ember/runloop";
export default function startApp(attrs) { export default function startApp(attrs) {
let application; let application;
// use defaults, but you can override // use defaults, but you can override
let attributes = Ember.assign({}, config.APP, attrs); let attributes = assign({}, config.APP, attrs);
Ember.run(() => { run(() => {
application = Application.create(attributes); application = Application.create(attributes);
application.setupForTesting(); application.setupForTesting();
application.injectTestHelpers(); application.injectTestHelpers();

View File

@ -3,9 +3,10 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>ManagerClient Tests</title> <title>Manager Client Tests</title>
<meta name="description" content=""> <meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta id="preloaded-data" data-preload="{&quot;rootUrl&quot;:&quot;/&quot;,&quot;longPollingBaseUrl&quot;:&quot;/&quot;}">
{{content-for "head"}} {{content-for "head"}}
{{content-for "test-head"}} {{content-for "test-head"}}

View File

@ -0,0 +1,120 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";
import { render, find } from "@ember/test-helpers";
import hbs from "htmlbars-inline-precompile";
import Repo from "manager-client/models/repo";
const repoObject = Repo.create({
unloaded: false,
branch: "origin/master",
id: "discourse",
name: "discourse",
official: false,
path: "/c/discourse",
pretty_version: "v2.2.0.beta6 +98",
url: "https://github.com/discourse/discourse",
version: "8f65e4f",
latest: {
commits_behind: 3,
date: "2018-12-26T20:52:07.000+03:00",
path: "/c/discourse",
pretty_version: "v2.2.0.beta6 +101",
version: "2b006c0"
}
});
const managerRepo = Repo.create({
unloaded: false,
branch: "origin/master",
id: "docker_manager",
name: "docker_manager",
official: true,
path: "/c/discourse/plugins/docker_manager",
pretty_version: null,
url: "https://github.com/discourse/docker_manager",
version: "0b1fb4b",
latest: {
commits_behind: 0,
date: "2018-12-26T20:52:07.000+03:00",
path: "/c/discourse/plugins/docker_manager",
pretty_version: null,
version: "0b1fb4b"
}
});
module("Integration | Component | repo-status", function(hooks) {
setupRenderingTest(hooks);
test("it renders correctly", async function(assert) {
this.set("repo", repoObject);
this.set("managerRepo", managerRepo);
await render(hbs`{{repo-status repo=repo managerRepo=managerRepo}}`);
assert.equal(
find("span.current.commit-hash").textContent.trim(),
"v2.2.0.beta6 +98",
"tag version is used when present"
);
assert.equal(
find("span.new.commit-hash").textContent.trim(),
"v2.2.0.beta6 +101",
"tag version is used when present"
);
assert.equal(
find("li.new-commits a").textContent.trim(),
"3 new commits",
"shows number of new commits"
);
assert.equal(
find("li.new-commits a").href.trim(),
"https://github.com/discourse/discourse/compare/8f65e4f...2b006c0",
"links to GitHub diff page"
);
this.set("repo.pretty_version", null);
this.set("repo.latest.pretty_version", null);
assert.equal(
find("span.current.commit-hash").textContent.trim(),
"8f65e4f",
"commit hash is used when tag version is absent"
);
assert.equal(
find("span.new.commit-hash").textContent.trim(),
"2b006c0",
"commit hash is used when tag version is absent"
);
assert
.dom("img.check-circle")
.doesNotExist("green check is absent when not official");
this.set("repo.official", true);
assert
.dom("img.check-circle")
.exists("green check is present when official");
assert
.dom("button.upgrade-button")
.exists("upgrade button is visibile when plugin is out-of-date");
assert.equal(
find("button.upgrade-button").disabled,
false,
"upgrade button is not disabled when docker_manager repo is up-to-date"
);
this.set("managerRepo.upToDate", false);
assert.equal(
find("button.upgrade-button").disabled,
true,
"upgrade button is disabled when docker_manager repo is out-of-date"
);
this.set("repo.latest.commits_behind", 0);
this.set("repo.version", "2b006c0");
this.set("repo.pretty_version", "v2.2.0.beta6 +101");
assert
.dom("button.upgrade-button")
.doesNotExist("upgrade button is not visibile when plugin is up-to-date");
});
});

View File

@ -1,6 +1,8 @@
import resolver from './helpers/resolver'; import Application from "../app";
import { import config from "../config/environment";
setResolver import { setApplication } from "@ember/test-helpers";
} from 'ember-qunit'; import { start } from "ember-qunit";
setResolver(resolver); setApplication(Application.create(config.APP));
start();

515
manager-client/vendor/message-bus.js vendored Normal file
View File

@ -0,0 +1,515 @@
/*jshint bitwise: false*/
(function(global, document, undefined) {
'use strict';
var previousMessageBus = global.MessageBus;
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
var callbacks, clientId, failCount, shouldLongPoll, queue, responseCallbacks, uniqueId, baseUrl;
var me, started, stopped, longPoller, pollTimeout, paused, later, jQuery, interval, chunkedBackoff;
var delayPollTimeout;
var ajaxInProgress = false;
uniqueId = function() {
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r, v;
r = Math.random() * 16 | 0;
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
clientId = uniqueId();
responseCallbacks = {};
callbacks = [];
queue = [];
interval = null;
failCount = 0;
baseUrl = "/";
paused = false;
later = [];
chunkedBackoff = 0;
jQuery = global.jQuery;
var hiddenProperty;
(function(){
var prefixes = ["","webkit","ms","moz"];
for(var i=0; i<prefixes.length; i++) {
var prefix = prefixes[i];
var check = prefix + (prefix === "" ? "hidden" : "Hidden");
if(document[check] !== undefined ){
hiddenProperty = check;
}
}
})();
var isHidden = function() {
if (hiddenProperty !== undefined){
return document[hiddenProperty];
} else {
return !document.hasFocus;
}
};
var hasLocalStorage = (function() {
try {
localStorage.setItem("mbTestLocalStorage", Date.now());
localStorage.removeItem("mbTestLocalStorage");
return true;
} catch(e) {
return false;
}
})();
var updateLastAjax = function() {
if (hasLocalStorage) {
localStorage.setItem("__mbLastAjax", Date.now());
}
}
var hiddenTabShouldWait = function() {
if (hasLocalStorage && isHidden()) {
var lastAjaxCall = parseInt(localStorage.getItem("__mbLastAjax"), 10);
var deltaAjax = Date.now() - lastAjaxCall;
return deltaAjax >= 0 && deltaAjax < me.minHiddenPollInterval;
}
return false;
}
var hasonprogress = (new XMLHttpRequest()).onprogress === null;
var allowChunked = function(){
return me.enableChunkedEncoding && hasonprogress;
};
shouldLongPoll = function() {
return me.alwaysLongPoll || !isHidden();
};
var totalAjaxFailures = 0;
var totalAjaxCalls = 0;
var lastAjax;
var processMessages = function(messages) {
var gotData = false;
if (!messages) return false; // server unexpectedly closed connection
for (var i=0; i<messages.length; i++) {
var message = messages[i];
gotData = true;
for (var j=0; j<callbacks.length; j++) {
var callback = callbacks[j];
if (callback.channel === message.channel) {
callback.last_id = message.message_id;
try {
callback.func(message.data, message.global_id, message.message_id);
}
catch(e){
if(console.log) {
console.log("MESSAGE BUS FAIL: callback " + callback.channel + " caused exception " + e.message);
}
}
}
if (message.channel === "/__status") {
if (message.data[callback.channel] !== undefined) {
callback.last_id = message.data[callback.channel];
}
}
}
}
return gotData;
};
var reqSuccess = function(messages) {
failCount = 0;
if (paused) {
if (messages) {
for (var i=0; i<messages.length; i++) {
later.push(messages[i]);
}
}
} else {
return processMessages(messages);
}
return false;
};
longPoller = function(poll, data) {
if (ajaxInProgress) {
// never allow concurrent ajax reqs
return;
}
var gotData = false;
var aborted = false;
lastAjax = new Date();
totalAjaxCalls += 1;
data.__seq = totalAjaxCalls;
var longPoll = shouldLongPoll() && me.enableLongPolling;
var chunked = longPoll && allowChunked();
if (chunkedBackoff > 0) {
chunkedBackoff--;
chunked = false;
}
var headers = {
'X-SILENCE-LOGGER': 'true'
};
for (var name in me.headers){
headers[name] = me.headers[name];
}
if (!chunked){
headers["Dont-Chunk"] = 'true';
}
var dataType = chunked ? "text" : "json";
var handle_progress = function(payload, position) {
var separator = "\r\n|\r\n";
var endChunk = payload.indexOf(separator, position);
if (endChunk === -1) {
return position;
}
var chunk = payload.substring(position, endChunk);
chunk = chunk.replace(/\r\n\|\|\r\n/g, separator);
try {
reqSuccess(JSON.parse(chunk));
} catch(e) {
if (console.log) {
console.log("FAILED TO PARSE CHUNKED REPLY");
console.log(data);
}
}
return handle_progress(payload, endChunk + separator.length);
}
var disableChunked = function(){
if (me.longPoll) {
me.longPoll.abort();
chunkedBackoff = 30;
}
};
var setOnProgressListener = function(xhr) {
var position = 0;
// if it takes longer than 3000 ms to get first chunk, we have some proxy
// this is messing with us, so just backoff from using chunked for now
var chunkedTimeout = setTimeout(disableChunked,3000);
xhr.onprogress = function () {
clearTimeout(chunkedTimeout);
if(xhr.getResponseHeader('Content-Type') === 'application/json; charset=utf-8') {
// not chunked we are sending json back
chunked = false;
return;
}
position = handle_progress(xhr.responseText, position);
}
};
if (!me.ajax){
throw new Error("Either jQuery or the ajax adapter must be loaded");
}
updateLastAjax();
ajaxInProgress = true;
var req = me.ajax({
url: me.baseUrl + "message-bus/" + me.clientId + "/poll" + (!longPoll ? "?dlp=t" : ""),
data: data,
cache: false,
async: true,
dataType: dataType,
type: 'POST',
headers: headers,
messageBus: {
chunked: chunked,
onProgressListener: function(xhr) {
var position = 0;
// if it takes longer than 3000 ms to get first chunk, we have some proxy
// this is messing with us, so just backoff from using chunked for now
var chunkedTimeout = setTimeout(disableChunked,3000);
return xhr.onprogress = function () {
clearTimeout(chunkedTimeout);
if(xhr.getResponseHeader('Content-Type') === 'application/json; charset=utf-8') {
chunked = false; // not chunked, we are sending json back
} else {
position = handle_progress(xhr.responseText, position);
}
}
}
},
xhr: function() {
var xhr = jQuery.ajaxSettings.xhr();
if (!chunked) {
return xhr;
}
this.messageBus.onProgressListener(xhr);
return xhr;
},
success: function(messages) {
if (!chunked) {
// we may have requested text so jQuery will not parse
if (typeof(messages) === "string") {
messages = JSON.parse(messages);
}
gotData = reqSuccess(messages);
}
},
error: function(xhr, textStatus, err) {
if(textStatus === "abort") {
aborted = true;
} else {
failCount += 1;
totalAjaxFailures += 1;
}
},
complete: function() {
ajaxInProgress = false;
var interval;
try {
if (gotData || aborted) {
interval = me.minPollInterval;
} else {
interval = me.callbackInterval;
if (failCount > 2) {
interval = interval * failCount;
} else if (!shouldLongPoll()) {
interval = me.backgroundCallbackInterval;
}
if (interval > me.maxPollInterval) {
interval = me.maxPollInterval;
}
interval -= (new Date() - lastAjax);
if (interval < 100) {
interval = 100;
}
}
} catch(e) {
if(console.log && e.message) {
console.log("MESSAGE BUS FAIL: " + e.message);
}
}
if (pollTimeout) {
clearTimeout(pollTimeout);
pollTimeout = null;
}
if (started) {
pollTimeout = setTimeout(function(){
pollTimeout = null;
poll();
}, interval);
}
me.longPoll = null;
}
});
return req;
};
me = {
/* shared between all tabs */
minHiddenPollInterval: 1500,
enableChunkedEncoding: true,
enableLongPolling: true,
callbackInterval: 15000,
backgroundCallbackInterval: 60000,
minPollInterval: 100,
maxPollInterval: 3 * 60 * 1000,
callbacks: callbacks,
clientId: clientId,
alwaysLongPoll: false,
baseUrl: baseUrl,
headers: {},
ajax: (jQuery && jQuery.ajax),
noConflict: function(){
global.MessageBus = global.MessageBus.previousMessageBus;
return this;
},
diagnostics: function(){
console.log("Stopped: " + stopped + " Started: " + started);
console.log("Current callbacks");
console.log(callbacks);
console.log("Total ajax calls: " + totalAjaxCalls + " Recent failure count: " + failCount + " Total failures: " + totalAjaxFailures);
console.log("Last ajax call: " + (new Date() - lastAjax) / 1000 + " seconds ago") ;
},
pause: function() {
paused = true;
},
resume: function() {
paused = false;
processMessages(later);
later = [];
},
stop: function() {
stopped = true;
started = false;
if (delayPollTimeout) {
clearTimeout(delayPollTimeout);
delayPollTimeout = null;
}
if (me.longPoll) {
me.longPoll.abort();
}
},
// Start polling
start: function() {
var poll;
if (started) return;
started = true;
stopped = false;
poll = function() {
var data;
if(stopped) {
return;
}
if (callbacks.length === 0 || hiddenTabShouldWait()) {
if(!delayPollTimeout) {
delayPollTimeout = setTimeout(function() {
delayPollTimeout = null;
poll();
}, parseInt(500 + Math.random() * 500));
}
return;
}
data = {};
for (var i=0;i<callbacks.length;i++) {
data[callbacks[i].channel] = callbacks[i].last_id;
}
// could possibly already be started
// notice the delay timeout above
if (!me.longPoll) {
me.longPoll = longPoller(poll, data);
}
};
// monitor visibility, issue a new long poll when the page shows
if(document.addEventListener && 'hidden' in document){
me.visibilityEvent = global.document.addEventListener('visibilitychange', function(){
if(!document.hidden && !me.longPoll && pollTimeout){
clearTimeout(pollTimeout);
clearTimeout(delayPollTimeout);
delayPollTimeout = null;
pollTimeout = null;
poll();
}
});
}
poll();
},
"status": function() {
if (paused) {
return "paused";
} else if (started) {
return "started";
} else if (stopped) {
return "stopped";
} else {
throw "Cannot determine current status";
}
},
// Subscribe to a channel
// if lastId is 0 or larger, it will recieve messages AFTER that id
// if lastId is negative it will perform lookbehind
// -1 will subscribe to all new messages
// -2 will recieve last message + all new messages
// -3 will recieve last 2 messages + all new messages
subscribe: function(channel, func, lastId) {
if(!started && !stopped){
me.start();
}
if (typeof(lastId) !== "number") {
lastId = -1;
}
if (typeof(channel) !== "string") {
throw "Channel name must be a string!";
}
callbacks.push({
channel: channel,
func: func,
last_id: lastId
});
if (me.longPoll) {
me.longPoll.abort();
}
return func;
},
// Unsubscribe from a channel
unsubscribe: function(channel, func) {
// TODO allow for globbing in the middle of a channel name
// like /something/*/something
// at the moment we only support globbing /something/*
var glob;
if (channel.indexOf("*", channel.length - 1) !== -1) {
channel = channel.substr(0, channel.length - 1);
glob = true;
}
var removed = false;
for (var i=callbacks.length-1; i>=0; i--) {
var callback = callbacks[i];
var keep;
if (glob) {
keep = callback.channel.substr(0, channel.length) !== channel;
} else {
keep = callback.channel !== channel;
}
if(!keep && func && callback.func !== func){
keep = true;
}
if (!keep) {
callbacks.splice(i,1);
removed = true;
}
}
if (removed && me.longPoll) {
me.longPoll.abort();
}
return removed;
}
};
global.MessageBus = me;
})(window, document);

8238
manager-client/yarn.lock Normal file

File diff suppressed because it is too large Load Diff