=== modified file 'app/app.js'
--- app/app.js 2013-04-22 13:46:15 +0000
+++ app/app.js 2013-04-22 22:53:42 +0000
@@ -239,6 +239,7 @@
// If no cfg is passed in use a default empty object so we don't blow up
// getting at things.
cfg = cfg || {};
+
// If this flag is true, start the application
// with the console activated.
var consoleEnabled = this.get('consoleEnabled');
@@ -378,6 +379,7 @@
if (Y.Lang.isValue(user) && Y.Lang.isValue(password)) {
this.env.login();
}
+ console.log('connectedChange dispatch');
this.dispatch();
}
}, this);
@@ -396,6 +398,7 @@
if (this.get('activeView')) {
this.get('activeView').render();
} else {
+ console.log('ready dispatch');
this.dispatch();
}
}, this);
@@ -465,7 +468,6 @@
*/
on_database_changed: function(evt) {
Y.log(evt, 'debug', 'App: Database changed');
-
var self = this;
var active = this.get('activeView');
@@ -481,6 +483,7 @@
if (active && active.name === 'EnvironmentView') {
active.rendered();
} else {
+ console.log('on db changed dispatch');
this.dispatch();
}
},
@@ -492,14 +495,75 @@
*/
show_unit: function(req) {
// This replacement honors service names that have a hyphen in them.
- var unit_id = req.params.id.replace(/^(\S+)-(\d+)$/, '$1/$2');
- var unit = this.db.units.getById(unit_id);
- if (unit) {
- // Once the unit is loaded we need to get the full details of the
- // service. Otherwise the relations data will not be available.
- var service = this.db.services.getById(unit.service);
- this._prefetch_service(service);
- }
+ var unitId = req.params.id.replace(/^(\S+)-(\d+)$/, '$1/$2');
+ var serviceName = unitId.split('/')[0];
+ var self = this;
+
+ var unit = this.db.units.getById(unitId);
+ var service = this.db.services.getById('serviceName');
+ var prefetched, skipRender = false;
+ if (service) {
+ prefetched = service.get('prefetch');
+ }
+ this._prefetch_service(serviceName, function(result) {
+ /**
+ Adds a notification if the unit you are trying to view
+ doesn't exist
+
+ @method showNoUnitNotification
+ @param {String} title of the notification.
+ @param {String} description of the warning message.
+ */
+ function showNoUnitNotification(title, description) {
+ self.db.notifications.add(
+ new models.Notification({
+ title: title,
+ message: description,
+ level: 'error'
+ })
+ );
+ }
+ if (result.err) {
+ showNoUnitNotification(
+ 'Service does not exist.',
+ 'The service you are attempting to view does not exist.');
+ skipRender = true;
+ self.fire('navigateTo', {
+ url: self.nsRouter.url({gui: '/'})
+ });
+ return;
+ } else {
+ unit = self.db.units.getById(unitId);
+ if (!unit) {
+ showNoUnitNotification(
+ 'Unit does not exist.',
+ 'The unit you are attempting to view does not exist.');
+
+ var guipath = self.nsRouter.parse(Y.getLocation().href).gui;
+ skipRender = true;
+ self.fire('navigateTo', {
+ url: self.nsRouter.url(
+ {gui: '/service/' + guipath.split('/')[2].split('-')[0]})
+ });
+ return;
+ } else {
+ self.renderUnitView.call(self, unit, req.query);
+ }
+ }
+ }, true);
+ if (!unit && !prefetched && !skipRender) {
+ this.renderUnitView();
+ }
+ },
+
+ /**
+ Calls showView for the unit view
+
+ @method renderUnitView
+ @param {Object} unit to display the data for.
+ @param {Object} query the query string object from the routing.
+ */
+ renderUnitView: function(unit, query) {
this.showView(
'unit',
// The querystring is used to handle highlighting relation rows in
@@ -508,7 +572,7 @@
unit: unit,
db: this.db,
env: this.env,
- querystring: req.query,
+ querystring: query,
landscape: this.landscape,
nsRouter: this.nsRouter });
},
@@ -517,27 +581,125 @@
* @method _prefetch_service
* @private
*/
- _prefetch_service: function(service) {
+ _prefetch_service: function(serviceId, callback, useCallback) {
+ if (typeof serviceId !== 'string') {
+ serviceId = serviceId.get('id');
+ }
+ var service = this.db.services.getById(serviceId),
+ self = this;
// Only prefetch once. We redispatch to the service view
// after we have status.
- if (!service || service.get('prefetch')) { return; }
- service.set('prefetch', true);
+ if (service) {
+ if (service.get('prefetch')) {
+ if (useCallback) {
+ callback(service);
+ return;
+ }
+ return service;
+ }
+ service.set('prefetch', true);
+ }
// Prefetch service details for service subviews.
- if (Y.Lang.isValue(service)) {
- if (!service.get('loaded')) {
- this.env.get_service(
- service.get('id'), Y.bind(this.loadService, this));
- }
- var charm_id = service.get('charm'),
- self = this;
- if (!Y.Lang.isValue(this.db.charms.getById(charm_id))) {
+ if (service) {
+ var charm_id = service.get('charm');
+ if (!this.db.charms.getById(charm_id)) {
this.db.charms.add({id: charm_id}).load(this.env,
// If views are bound to the charm model, firing "update" is
// unnecessary, and potentially even mildly harmful.
function(err, result) { self.db.fire('update'); });
}
}
+
+ if (!service || !service.get('loaded')) {
+
+ this.env.get_service(serviceId, function(result) {
+ if (result.err) {
+ // The service doesn't exist
+ if (typeof callback === 'function') {
+ callback(result);
+ }
+ } else {
+ var service = self.db.services.getById(result.service_name);
+ service.setAttrs({'config': result.result.config,
+ 'constraints': result.result.constraints,
+ 'loaded': true,
+ 'prefetch': service.get('prefetch') || false});
+ if (typeof callback === 'function') {
+ callback(service);
+ }
+
+ }
+ });
+ }
+ },
+
+ /**
+ Promise executor function for resolving a service model
+
+ @method _servicePromise
+ @param {String} serviceId of the service model to pull.
+ @param {Object} db object reference.
+ @param {Object} env object reference.
+ @param {Function} resolve function passed in by promise.
+ @param {Function} reject function passed in by promise.
+ @protected
+ */
+ _servicePromise: function(serviceId, db, env, resolve, reject) {
+ var service = db.services.getById(serviceId);
+ // Only prefetch once. We redispatch to the service view
+ // after we have status.
+ if (service) {
+ if (service.get('prefetch')) {
+ resolve(service);
+ }
+ service.set('prefetch', true);
+ var charm_id = service.get('charm');
+ if (!db.charms.getById(charm_id)) {
+ db.charms.add({id: charm_id}).load(env,
+ // If views are bound to the charm model, firing "update" is
+ // unnecessary, and potentially even mildly harmful.
+ function(err, result) { db.fire('update'); });
+ }
+ }
+
+ if (!service || !service.get('loaded')) {
+ env.get_service(serviceId, function(result) {
+ if (result.err) {
+ // The service doesn't exist
+ reject(result);
+ } else {
+ var service = db.services.getById(result.service_name);
+ service.setAttrs({
+ 'config': result.result.config,
+ 'constraints': result.result.constraints,
+ 'loaded': true,
+ 'prefetch': service.get('prefetch') || false
+ });
+ resolve(service);
+ }
+ });
+ }
+ },
+
+ /**
+ Returns either a service model or a promise to return the service
+ model when it's available.
+
+ @method getServiceModel
+ @param {String} serviceId of the service model you need.
+ @return {Y.Model | Y.Promise} If the service model is readily available
+ it will return the model, else it will return a promise which goes
+ and fetches and updates the service model.
+ */
+ getServiceModel: function(serviceId) {
+ var service = this.db.services.getById(serviceId);
+ if (service && service.get('prefetch')) {
+ return service;
+ } else {
+ return new Y.Promise(
+ Y.bind(this._servicePromise, null, serviceId, this.db, this.env));
+ }
},
/**
@@ -545,10 +707,8 @@
* @private
*/
_buildServiceView: function(req, viewName) {
- var service = this.db.services.getById(req.params.id);
- this._prefetch_service(service);
this.showView(viewName, {
- model: service,
+ model: this.getServiceModel(req.params.id),
db: this.db,
env: this.env,
landscape: this.landscape,
@@ -769,6 +929,7 @@
spinner.stop();
}
}
+ console.log('onlogin dispatch');
this.dispatch();
} else {
this.show_login();
@@ -849,6 +1010,7 @@
* @method loadService
*/
loadService: function(evt) {
+ // no longer called by _prefetch_service ... moving away from dispatching
if (evt.err) {
this.db.notifications.add(
new models.Notification({
@@ -872,6 +1034,7 @@
'constraints': svc_data.constraints,
'loaded': true,
'prefetch': false});
+ console.log('load service dispatch');
this.dispatch();
},
@@ -1058,6 +1221,7 @@
'json-parse',
'app-base',
'app-transitions',
+ 'promise',
'base',
'node',
'model',
=== modified file 'app/views/service.js'
--- app/views/service.js 2013-04-18 21:30:29 +0000
+++ app/views/service.js 2013-04-22 22:53:42 +0000
@@ -417,7 +417,9 @@
* @class ServiceRelationsView
*/
views.service_relations = Y.Base.create(
- 'ServiceRelationsView', ServiceViewBase, [views.JujuBaseView], {
+ 'ServiceRelationsView', ServiceViewBase,
+ [views.JujuBaseView,
+ views.serviceViewPromiseSupport], {
template: Templates['service-relations'],
@@ -462,18 +464,6 @@
};
},
- render: function() {
- var container = this.get('container');
- var service = this.get('model');
- if (!service || !service.get('loaded')) {
- container.setHTML('
Loading...
');
- console.log('waiting on service data');
- } else {
- container.setHTML(this.template(this.gatherRenderData()));
- }
- return this;
- },
-
confirmRemoved: function(ev) {
// We wait to make the panel until now, because in the render method
// the container is not yet part of the document.
@@ -553,7 +543,9 @@
* @class ServiceConstraintsView
*/
views.service_constraints = Y.Base.create(
- 'ServiceConstraintsView', ServiceViewBase, [views.JujuBaseView], {
+ 'ServiceConstraintsView', ServiceViewBase,
+ [views.JujuBaseView,
+ views.serviceViewPromiseSupport], {
template: Templates['service-constraints'],
@@ -664,12 +656,6 @@
charm_id: charm_id,
serviceIsJujuGUI: utils.isGuiCharmUrl(charm_id)
};
- },
-
- render: function() {
- var container = this.get('container');
- container.setHTML(this.template(this.gatherRenderData()));
- return this;
}
});
@@ -678,7 +664,9 @@
* @class ServiceConfigView
*/
views.service_config = Y.Base.create(
- 'ServiceConfigView', ServiceViewBase, [views.JujuBaseView], {
+ 'ServiceConfigView', ServiceViewBase,
+ [views.JujuBaseView,
+ views.serviceViewPromiseSupport], {
template: Templates['service-config'],
@@ -744,18 +732,6 @@
};
},
- render: function() {
- var container = this.get('container');
- var service = this.get('model');
- if (!service || !service.get('loaded')) {
- container.setHTML('
Loading...
');
- console.log('waiting on service data');
- } else {
- container.setHTML(this.template(this.gatherRenderData()));
- }
- return this;
- },
-
showErrors: function(errors) {
var container = this.get('container');
container.one('#save-service-config').removeAttribute('disabled');
@@ -887,7 +863,8 @@
* @class ServiceView
*/
var ServiceView = Y.Base.create('ServiceView', ServiceViewBase,
- [views.JujuBaseView], {
+ [views.JujuBaseView,
+ views.serviceViewPromiseSupport], {
template: Templates.service,
@@ -937,21 +914,6 @@
};
},
- render: function() {
- var container = this.get('container');
- var service = this.get('model');
- var env = this.get('db').environment.get('annotations');
- if (!service || !service.get('loaded')) {
- container.setHTML('
Loading...
');
- console.log('waiting on service data');
- } else {
- container.setHTML(this.template(this.gatherRenderData()));
- views.utils.updateLandscapeBottomBar(this.get('landscape'),
- env, service, container);
- }
- return this;
- },
-
filterUnits: function(filter_state, units) {
// If filtering was requested, do it.
if (filter_state) {
=== modified file 'app/views/topology/service.js'
--- app/views/topology/service.js 2013-04-20 03:04:47 +0000
+++ app/views/topology/service.js 2013-04-22 22:53:42 +0000
@@ -1134,7 +1134,12 @@
topo.detachContainer();
topo.fire('navigateTo', {
- url: getModelURL(service)
+<<<<<<< TREE
+ url: getModelURL(service)
+=======
+ // getModelURL returns a namespaced string
+ url: getModelURL(service)
+>>>>>>> MERGE-SOURCE
});
},
=== modified file 'app/views/unit.js'
--- app/views/unit.js 2013-04-16 11:08:16 +0000
+++ app/views/unit.js 2013-04-22 22:53:42 +0000
@@ -31,14 +31,16 @@
render: function() {
var container = this.get('container');
var unit = this.get('unit');
+ var nsRouter = this.get('nsRouter');
+ var db = this.get('db');
+
if (!unit) {
- container.setHTML('
Loading...
');
+ container.setHTML('
Loading unit details...
');
console.log('waiting on unit data');
return this;
}
- var db = this.get('db'),
- service = db.services.getById(unit.service),
+ var service = db.services.getById(unit.service),
env = db.environment.get('annotations');
if (!service.get('loaded')) {
@@ -88,7 +90,6 @@
});
var charmAttrs = charm.getAttrs();
- var nsRouter = this.get('nsRouter');
container.setHTML(this.template({
charmUri: nsRouter.url({
@@ -356,4 +357,5 @@
'handlebars',
'node',
'juju-view-utils',
+ 'promise',
'view']});
=== modified file 'app/views/utils.js'
--- app/views/utils.js 2013-04-15 15:45:18 +0000
+++ app/views/utils.js 2013-04-22 22:53:42 +0000
@@ -232,6 +232,137 @@
if (!text || text === undefined) {return '';}
return new Y.Handlebars.SafeString(humanizeTimestamp(Number(text)));
});
+
+ /**
+ Constructor function for serviceViewPromiseSupport extension
+ */
+ function serviceViewPromiseSupport() {}
+
+ serviceViewPromiseSupport.prototype = {
+
+ /**
+ Sets the dataRendered flag and attaches a listener to re render the view
+ when the model is changed
+
+ @method initializer
+ */
+ initializer: function() {
+ // flag to ensure that we only render once per instantiation
+ this.dataRendered = false;
+
+ this.after('modelChange', function() {
+ this.render();
+ }, this);
+ },
+ /**
+ Resolve callback for the model promise which sets the model
+ data returned from a successful promise request.
+
+ @method _serviceDataReceived
+ @param {Object} model service model instance.
+ */
+ _serviceDataReceived: function(model) {
+ // set the model with the data returned from the promise
+ this.set('model', model);
+ },
+
+ /**
+ Reject callback for the model promise which creates an error
+ notification and then redirects the user to the evironment view
+
+ @method _noServiceAvailable
+ */
+ _noServiceAvailable: function() {
+ this.get('db').notifications.add(
+ new Y.juju.models.Notification({
+ title: 'Service is not available',
+ message: 'The service you are trying to view does not exist',
+ level: 'error'
+ })
+ );
+
+ this.fire('navigateTo', {
+ url: this.get('nsRouter').url({gui: '/'})
+ });
+ },
+
+ /**
+ Shared rendering method to render the loading service data view
+
+ @method renderLoading
+ */
+ renderLoading: function() {
+ var container = this.get('container');
+ container.setHTML('
Loading service details...
');
+ console.log('waiting on service data');
+ },
+
+ /**
+ Shared rendering method to render the service data view
+
+ @method renderData
+ */
+ renderData: function() {
+ var container = this.get('container');
+ var service = this.get('model');
+ var db = this.get('db');
+ var env = db.environment.get('annotations');
+ container.setHTML(this.template(this.gatherRenderData()));
+ // to be able to use this same method for all service views
+ if (container.one('.landscape-controls')) {
+ views.utils.updateLandscapeBottomBar(this.get('landscape'),
+ env, service, container);
+ }
+ },
+
+ /**
+ Shared render method to be used in service detail views
+
+ @method render
+ @return {Object} view instance.
+ */
+ render: function() {
+ var container = this.get('container');
+ var service = this.get('model');
+ var db = this.get('db');
+ var env = db.environment.get('annotations');
+ if (!service || !service.get('loaded')) {
+ this.renderLoading();
+ } else {
+ // Because this render method gets called multiple times on load
+ // but we only want to render it again once the service model
+ // promise returns and we have data available we put it behind
+ // a single render check.
+ if (!this.dataRendered) {
+ this.dataRendered = true;
+ this.renderData();
+ this.fitToWindow();
+ }
+ }
+ return this;
+ }
+ };
+
+ serviceViewPromiseSupport.ATTRS = {
+ model: {
+ 'setter': function(value) {
+ // This allows us to pass a promise or a model
+ // into the model attribute.
+ if (Y.Promise.isPromise(value)) {
+ value.then(
+ Y.bind(this._serviceDataReceived, this),
+ Y.bind(this._noServiceAvailable, this));
+ // This tells the attribute validator not to
+ // set the model with the promise.
+ return Y.Attribute.INVALID_VALUE;
+ }
+ return value;
+ }
+ }
+ };
+
+ views.serviceViewPromiseSupport = serviceViewPromiseSupport;
+
var JujuBaseView = Y.Base.create('JujuBaseView', Y.Base, [], {
bindModelView: function(model) {
=== modified file 'test/test_application_notifications.js'
--- test/test_application_notifications.js 2013-04-12 08:14:07 +0000
+++ test/test_application_notifications.js 2013-04-22 22:53:42 +0000
@@ -261,7 +261,6 @@
it('should show notification for "get_service" exceptions' +
' (service constraints view)', function() {
-
var view = new views.service_constraints(
{ model:
{ getAttrs: NO_OP,
@@ -275,7 +274,8 @@
db: db,
env: {set_constraints: willError},
container: viewContainer}).render();
-
+ view.get('container').append(
+ '');
view.updateConstraints();
assert.equal(1, db.notifications.size());
=== modified file 'test/test_service_view.js'
--- test/test_service_view.js 2013-04-12 08:30:04 +0000
+++ test/test_service_view.js 2013-04-22 22:53:42 +0000
@@ -262,7 +262,7 @@
callbacks.length.should.equal(1);
// Since we don't have an app to listen to this event and tell the
// view to re-render, we need to do it ourselves.
- db.on('update', view.render, view);
+ db.on('update', view.renderData, view);
callbacks[0]({result: [new_unit_id]});
var db_names = db.units.map(function(u) {return u.id;});
db_names.sort();
@@ -406,6 +406,7 @@
// event. Here we should call it manually because we have no
// "route" for this test.
view.render();
+ view.renderData();
assert.isTrue(dbUpdated);
assert.isNotNull(container.one(selectorAfter));
@@ -735,7 +736,7 @@
view.get('model').set('loaded', false);
view.render();
var html = container.getHTML();
- assert.match(html, /Loading\.\.\./);
+ assert.match(html, /Loading/);
});
it('loading message if the service is not loaded (relations)', function() {
@@ -745,7 +746,7 @@
view.get('model').set('loaded', false);
view.render();
var html = container.getHTML();
- assert.match(html, /Loading\.\.\./);
+ assert.match(html, /Loading/);
});
});
@@ -808,14 +809,14 @@
view.get('model').set('loaded', false);
view.render();
var html = container.getHTML();
- assert.match(html, /Loading\.\.\./);
+ assert.match(html, /Loading/);
});
it('displays no loading message if the service is loaded', function() {
view.get('model').set('loaded', true);
view.render();
var html = container.getHTML();
- assert.notMatch(html, /Loading\.\.\./);
+ assert.notMatch(html, /Loading/);
});
it('informs the template if the service is the GUI', function() {
=== modified file 'undocumented'
--- undocumented 2013-04-05 00:19:36 +0000
+++ undocumented 2013-04-22 22:53:42 +0000
@@ -87,11 +87,9 @@
app/views/unit.js:189 "doRemoveUnit"
app/views/service.js:838 "_setConfigCallback"
app/views/service.js:191 "destroyService"
-app/views/service.js:464 "render"
app/views/service.js:963 "filterUnits"
app/views/service.js:498 "doRemoveRelation"
app/views/service.js:175 "confirmDestroy"
-app/views/service.js:756 "render"
app/views/service.js:390 "fitToWindow"
app/views/service.js:525 "_removeRelationCallback"
app/views/service.js:476 "confirmRemoved"
@@ -99,7 +97,6 @@
app/views/service.js:62 "_modifyUnits"
app/views/service.js:40 "modifyUnits"
app/views/service.js:126 "_removeUnitCallback"
-app/views/service.js:948 "render"
app/views/service.js:768 "showErrors"
app/views/service.js:98 "_addUnitCallback"
app/views/service.js:563 "updateConstraints"
@@ -107,7 +104,6 @@
app/views/service.js:339 "initializer"
app/views/service.js:808 "saveConfig"
app/views/service.js:351 "getServiceTabs"
-app/views/service.js:678 "render"
app/views/service.js:589 "_setConstraintsCallback"
app/views/service.js:391 "getHeight"
app/views/topology/panzoom.js:52 "renderSlider"