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
manager-client/vendor/*
*.sublime-workspace
.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
* Install `node.js`, `bower`, `grunt`
* Install `node.js` and `yarn`
* 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`)
* 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:
* `cd manager-client`
* `npm install`
* `bower install`
* Use `ember server --proxy "http://localhost:3000"` to proxy to your Discourse instance running on port 3000
* `yarn install`
* Make sure your local Discourse instance is running at 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`
* 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
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
1. Fork it

View File

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

View File

@ -7,25 +7,7 @@
<meta name="description" content="">
<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" />
<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>
<meta id="preloaded-data" data-preload="<%= { rootUrl: discourse_root_url, longPollingBaseUrl: long_polling_base_url }.to_json %>">
<%= javascript_include_tag "docker-manager-vendor" %>
<%= 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}"
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
commit_date(latest_origin_commit)
end
@ -83,6 +91,16 @@ class DockerManager::GitRepo
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
@upgrade_key ||= "upgrade:#{path}"
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/)
* [Node.js](https://nodejs.org/) (with NPM)
* [Bower](https://bower.io/)
* [Yarn](https://yarnpkg.com)
* [Ember CLI](https://ember-cli.com/)
* [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
* `cd manager-client`
* `npm install`
* `bower install`
* `yarn install`
## Running / Development
* `ember serve`
* `./dev_server` (in root directory of this plugin)
* Visit your app at [http://localhost:4200](http://localhost:4200).
### 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 --environment production` (production)
### Deploying
Specify what it takes to deploy your app.
## Further Reading / Useful Links
* [ember.js](http://emberjs.com/)

View File

@ -1,13 +1,14 @@
import Ember from 'ember';
import Resolver from './resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';
import Ember from "ember";
import Resolver from "./resolver";
import loadInitializers from "ember-load-initializers";
import config from "./config/environment";
import Application from "@ember/application";
let App;
Ember.MODEL_FACTORY_INJECTIONS = true;
App = Ember.Application.extend({
App = Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
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({
classNameBindings: [':progress', ':progress-striped', 'active'],
export default Component.extend({
classNameBindings: [":progress", ":progress-striped", "active"],
active: function() {
return parseInt(this.get('percent'), 10) !== 100;
}.property('percent'),
active: computed("percent", function() {
return parseInt(this.get("percent"), 10) !== 100;
}),
barStyle: function() {
let percent = parseInt(this.get('percent'), 10);
if (percent > 0) {
if (percent > 100) { percent = 100; }
return ('width: ' + this.get('percent') + '%').htmlSafe();
barStyle: computed("percent", function() {
let percent = parseInt(this.get("percent"), 10);
if (percent > 0) {
if (percent > 100) {
percent = 100;
}
return ("width: " + this.get("percent") + "%").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({
tagName: 'tr',
export default Component.extend({
router: service(),
tagName: "tr",
upgradeDisabled: function() {
const upgradingRepo = this.get('upgradingRepo');
upgradeDisabled: computed(
"upgradingRepo",
"repo",
"managerRepo",
"managerRepo.upToDate",
function() {
const upgradingRepo = this.get("upgradingRepo");
if (!upgradingRepo) {
const managerRepo = this.get('managerRepo');
if (!managerRepo) { return false; }
return (!managerRepo.get('upToDate')) && managerRepo !== this.get('repo');
if (!upgradingRepo) {
const managerRepo = this.get("managerRepo");
if (!managerRepo) {
return false;
}
return !managerRepo.get("upToDate") && managerRepo !== this.get("repo");
}
return true;
}
return true;
}.property('upgradingRepo', 'repo', 'managerRepo', 'managerRepo.upToDate'),
),
officialRepoImageSrc: function() {
if (!this.get('repo.official')) { return; }
return Discourse.getURL("/plugins/docker_manager/images/font-awesome-check-circle.png");
}.property('repo.official'),
officialRepoImageSrc: computed("repo.official", function() {
if (!this.get("repo.official")) {
return;
}
return Discourse.getURL(
"/plugins/docker_manager/images/font-awesome-check-circle.png"
);
}),
actions: {
upgrade: function() {
this.sendAction('upgrade', this.get('repo'));
upgrade() {
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({
classNameBindings: [':logs'],
export default Component.extend({
classNameBindings: [":logs"],
_outputChanged: function() {
Ember.run.scheduleOnce('afterRender', this, '_scrollBottom');
}.observes('output'),
_outputChanged: observer("output", function() {
scheduleOnce("afterRender", this, "_scrollBottom");
}),
_scrollBottom: function() {
if (this.get('followOutput')) {
_scrollBottom() {
if (this.get("followOutput")) {
this.$().scrollTop(this.$()[0].scrollHeight);
}
},
_scrollOnInsert: function() {
didInsertElement() {
this._super(...arguments);
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 Ember.Controller.extend({
showBanner: function(){
if(this.get("bannerDismissed")){
export default Controller.extend({
showBanner: computed("banner", "bannerDismissed", "banner.[]", function() {
if (this.get("bannerDismissed")) {
return false;
}
const banner = this.get("banner");
return banner && banner.length > 0;
}.property("banner", "bannerDismissed", "banner.@each"),
}),
appendBannerHtml: function(html){
appendBannerHtml(html) {
const banner = this.get("banner") || [];
if(banner.indexOf(html) === -1){
if (banner.indexOf(html) === -1) {
banner.pushObject(html);
}
this.set("banner", banner);
},
logoUrl: function() {
return Discourse.getURL("/assets/images/docker-manager-aff8eaea0445c0488c19f8cfd14faa8c2b278924438f19048eacc175d7d134e4.png");
}.property(),
logoUrl: computed(function() {
return Discourse.getURL(
"/assets/images/docker-manager-aff8eaea0445c0488c19f8cfd14faa8c2b278924438f19048eacc175d7d134e4.png"
);
}),
returnToSiteUrl: function() {
returnToSiteUrl: computed(function() {
return Discourse.getURL("/");
}.property(),
}),
backupsUrl: function() {
backupsUrl: computed(function() {
return Discourse.getURL("/admin/backups");
}.property(),
}),
actions: {
dismiss: function () {
dismiss() {
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,
upgrading: null,
upgradeAllButtonDisabled: function () {
return !this.get("managerRepo.upToDate") || this.get("allUpToDate");
}.property("managerRepo.upToDate", "allUpToDate"),
upgradeAllButtonDisabled: computed(
"managerRepo.upToDate",
"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"));
}.property("model.@each.upToDate"),
}),
actions: {
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,
init: function() {
init() {
this._super();
var self = this;
window.setInterval(function() {
self.performRefresh();
window.setInterval(() => {
this.performRefresh();
}, 5000);
},
performRefresh: function() {
if (this.get('autoRefresh')) {
this.get('model').refresh();
performRefresh() {
if (this.get("autoRefresh")) {
this.get("model").refresh();
}
}
});

View File

@ -1,8 +1,10 @@
/* 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,
init() {
@ -10,24 +12,24 @@ export default Ember.Controller.extend({
this.reset();
},
complete: Ember.computed.equal('status', 'complete'),
failed: Ember.computed.equal('status', 'failed'),
complete: equal("status", "complete"),
failed: equal("status", "failed"),
multiUpgrade: function() {
multiUpgrade: computed("model.length", function() {
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");
}.property("model.@each.name"),
}),
isUpToDate: function () {
isUpToDate: computed("model.[].upToDate", function() {
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"));
}.property("model.@each.upgrading"),
}),
repos() {
const model = this.get("model");
@ -36,43 +38,45 @@ export default Ember.Controller.extend({
updateAttribute(key, value, valueIsKey = false) {
this.get("model").forEach(repo => {
value = valueIsKey ? repo.get(value) : value;
value = valueIsKey ? repo.get(value) : value;
repo.set(key, value);
});
},
messageReceived(msg) {
switch(msg.type) {
switch (msg.type) {
case "log":
this.set('output', this.get('output') + msg.value + "\n");
this.set("output", this.get("output") + msg.value + "\n");
break;
case "percent":
this.set('percent', msg.value);
this.set("percent", msg.value);
break;
case "status":
this.set('status', msg.value);
this.set("status", msg.value);
if (msg.value === "complete") {
this.get("model").filter(repo => repo.get("upgrading")).forEach(repo => {
repo.set("version", repo.get("latest.version"));
});
this.get("model")
.filter(repo => repo.get("upgrading"))
.forEach(repo => {
repo.set("version", repo.get("latest.version"));
});
}
if (msg.value === 'complete' || msg.value === 'failed') {
this.updateAttribute('upgrading', false);
if (msg.value === "complete" || msg.value === "failed") {
this.updateAttribute("upgrading", false);
}
break;
}
},
upgradeButtonText: function() {
upgradeButtonText: computed("upgrading", function() {
if (this.get("upgrading")) {
return "Upgrading...";
} else {
return "Start Upgrading";
}
}.property("upgrading"),
}),
startBus() {
MessageBus.subscribe("/docker/upgrade", msg => {
@ -85,7 +89,7 @@ export default Ember.Controller.extend({
},
reset() {
this.setProperties({ output: '', status: null, percent: 0 });
this.setProperties({ output: "", status: null, percent: 0 });
},
actions: {
@ -93,33 +97,41 @@ export default Ember.Controller.extend({
this.reset();
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();
}
const repo = this.get('model')[0];
if (repo.get('upgrading')) { return; }
const repo = this.get("model")[0];
if (repo.get("upgrading")) {
return;
}
repo.startUpgrade();
},
resetUpgrade() {
bootbox.confirm("WARNING: You should only reset upgrades that have failed and are not running.\n\n"+
"This will NOT cancel currently running builds and should only be used as a last resort.", result => {
if (result) {
if (this.get("multiUpgrade")) {
return Repo.resetAll(this.get("model").filter(repo => !repo.get("upToDate"))).finally(() => {
bootbox.confirm(
"WARNING: You should only reset upgrades that have failed and are not running.\n\n" +
"This will NOT cancel currently running builds and should only be used as a last resort.",
result => {
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.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;
if (parseInt(commitsBehind) === 0) {
@ -11,12 +13,12 @@ export default Ember.Helper.helper(function(params) {
commitsBehind === 1 ? "" : "s"
}`;
if (Ember.isNone(url)) {
if (isNone(url)) {
return description;
}
var _url = url.substr(0, url.search(/(\.git)?$/));
description = `<a href='${_url}/compare/${oldSha}..${newSha}'>${description}</a>`;
const _url = url.substr(0, url.search(/(\.git)?$/));
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>
<meta name="description" content="">
<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'}}
@ -14,17 +15,6 @@
{{content-for 'head-footer'}}
<script>
window.Discourse = {
rootUrl: '/',
longPollingBaseUrl: '/',
getURL: function(url) {
return url;
}
};
</script>
</head>
<body>
{{content-for 'body'}}

View File

@ -1,15 +1,14 @@
/* global $:true, Discourse */
import request from 'ember-ajax/request';
import Discourse from "manager-client/discourse";
export default {
name: "findCsrfToken",
initialize() {
return request(Discourse.getURL('/session/csrf')).then(function(result) {
var token = result.csrf;
$.ajaxPrefilter(function(options, originalOptions, xhr) {
return Em.$.ajax(Discourse.getURL("/session/csrf")).then(result => {
const token = result.csrf;
Em.$.ajaxPrefilter((options, originalOptions, xhr) => {
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 {
name: "message-bus",
@ -6,14 +6,16 @@ export default {
initialize() {
MessageBus.baseUrl = Discourse.longPollingBaseUrl;
if (MessageBus.baseUrl !== '/') {
if (MessageBus.baseUrl !== "/") {
MessageBus.ajax = function(opts) {
opts.headers = opts.headers || {};
opts.headers['X-Shared-Session-Key'] = $('meta[name=shared_session_key]').attr('content');
return $.ajax(opts);
opts.headers["X-Shared-Session-Key"] = Em.$(
"meta[name=shared_session_key]"
).attr("content");
return Em.$.ajax(opts);
};
} 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';
import Ember from 'ember';
const ProcessList = Ember.Object.extend({
const ProcessList = EmberObject.extend({
output: null,
refresh() {
return request(Discourse.getURL("/admin/docker/ps"), {dataType: 'text'}).then(result => {
this.set('output', result);
return Em.$.ajax(Discourse.getURL("/admin/docker/ps"), {
dataType: "text"
}).then(result => {
this.set("output", result);
return this;
});
}

View File

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

View File

@ -1,3 +1,3 @@
import Resolver from 'ember-resolver';
import Resolver from "ember-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,
rootURL: config.rootURL
});
Router.map(function() {
this.route("processes");
this.route('upgrade', { path: '/upgrade/:id' });
this.route("upgrade", { path: "/upgrade/:id" });
});
export default Router;

View File

@ -1,13 +1,15 @@
import Repo from 'manager-client/models/repo';
import Ember from 'ember';
import Repo from "manager-client/models/repo";
import Route from "@ember/routing/route";
export default Ember.Route.extend({
export default Route.extend({
model() {
return Repo.findAll();
},
loadRepos(list) {
if (list.length === 0) { return; }
if (list.length === 0) {
return;
}
this.loadRepo(list.shift()).then(() => this.loadRepos(list));
},
@ -16,31 +18,30 @@ export default Ember.Route.extend({
},
setupController(controller, model) {
const applicationController = this.controllerFor('application');
const applicationController = this.controllerFor("application");
controller.setProperties({ model, upgrading: null });
model.forEach(repo => {
if (repo.get('upgrading')) {
controller.set('upgrading', repo);
if (repo.get("upgrading")) {
controller.set("upgrading", repo);
}
// Special case: Upgrade docker manager first
if (repo.get('id') === 'docker_manager') {
controller.set('managerRepo', repo);
if (repo.get("id") === "docker_manager") {
controller.set("managerRepo", repo);
}
// Special case: If the branch is "master" warn user
if (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>.");
if (
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));
},
actions: {
upgrade(repo) {
this.transitionTo('upgrade', repo);
}
}
});

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
<td>
{{#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}}
</td>
<td>
{{repo.name}}
{{repo.version}}
<a href="{{repo.url}}">{{repo.name}}</a>
<span class="current commit-hash">{{repo.prettyVersion}}</span>
</td>
<td>
{{#if repo.checkingStatus}}
@ -16,7 +16,7 @@
<div class='new-version'>
<h4>New Version Available!</h4>
<ul>
<li>Remote Version: {{repo.latest.version}}</li>
<li>Remote Version: <span class="new commit-hash">{{repo.prettyLatestVersion}}</span></li>
<li>Last Updated:
{{#if repo.latest.date}}
{{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>
</ul>
{{#if repo.upgrading}}
<button class="btn" {{action "upgrade"}}>Currently Upgrading...</button>
<button class="btn" disabled=true>Currently Upgrading...</button>
{{else}}
<button class="btn" {{action "upgrade"}} disabled={{upgradeDisabled}}>Upgrade</button>
<button class="upgrade-button btn" {{action "upgrade"}} disabled={{upgradeDisabled}}>Upgrade</button>
{{/if}}
</div>
{{/if}}

View File

@ -14,7 +14,7 @@
</tr>
<tbody>
{{#each model as |repo|}}
{{repo-status repo=repo upgradingRepo=upgrading managerRepo=managerRepo upgrade="upgrade"}}
{{repo-status repo=repo upgradingRepo=upgrading managerRepo=managerRepo}}
{{/each}}
</tbody>
</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.rootElement = '#ember-testing';
ENV.APP.autoboot = false;
}
if (environment === 'production') {

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -15,31 +15,43 @@
"test": "ember test"
},
"devDependencies": {
"broccoli-asset-rev": "^2.4.5",
"ember-ajax": "2.5.3",
"ember-cli": "2.10.0",
"ember-cli-app-version": "^2.0.0",
"ember-cli-babel": "^5.1.7",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-htmlbars": "^1.0.10",
"ember-cli-htmlbars-inline-precompile": "^0.3.3",
"ember-cli-inject-live-reload": "^1.4.1",
"ember-cli-jshint": "^2.0.1",
"ember-cli-moment-shim": "3.0.1",
"ember-cli-qunit": "^3.0.1",
"ember-cli-release": "^0.2.9",
"ember-cli-sass": "6.1.0",
"ember-cli-sri": "^2.1.0",
"ember-cli-test-loader": "^1.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-export-application-global": "^1.0.5",
"ember-load-initializers": "^0.5.1",
"ember-moment": "7.3.0",
"ember-resolver": "^2.0.3",
"loader.js": "^4.0.10"
"@ember/jquery": "^0.5.2",
"@ember/optional-features": "^0.6.3",
"broccoli-asset-rev": "^2.7.0",
"ember-ajax": "^4.0.1",
"ember-cli": "~3.6.0",
"ember-cli-app-version": "^3.2.0",
"ember-cli-babel": "^7.1.2",
"ember-cli-dependency-checker": "^3.0.0",
"ember-cli-eslint": "^4.2.3",
"ember-cli-htmlbars": "^3.0.0",
"ember-cli-htmlbars-inline-precompile": "^1.0.3",
"ember-cli-inject-live-reload": "^1.8.2",
"ember-cli-moment-shim": "3.7.1",
"ember-cli-sass": "8.0.1",
"ember-cli-sri": "^2.1.1",
"ember-cli-template-lint": "^1.0.0-beta.1",
"ember-cli-uglify": "^2.1.0",
"ember-data": "~3.6.0",
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^1.1.0",
"ember-maybe-import-regenerator": "^0.1.6",
"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": {
"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) {
Ember.run(application, 'destroy');
run(application, "destroy");
}

View File

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

View File

@ -1,14 +1,15 @@
import Ember from 'ember';
import Application from '../../app';
import config from '../../config/environment';
import Application from "../../app";
import config from "../../config/environment";
import { assign } from "@ember/polyfills";
import { run } from "@ember/runloop";
export default function startApp(attrs) {
let application;
// 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.setupForTesting();
application.injectTestHelpers();

View File

@ -3,9 +3,10 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>ManagerClient Tests</title>
<title>Manager Client Tests</title>
<meta name="description" content="">
<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 "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 {
setResolver
} from 'ember-qunit';
import Application from "../app";
import config from "../config/environment";
import { setApplication } from "@ember/test-helpers";
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