Merge lp:~bcsaller/juju-gui/namespace-routing into lp:juju-gui/experimental

Proposed by Benjamin Saller
Status: Merged
Merged at revision: 391
Proposed branch: lp:~bcsaller/juju-gui/namespace-routing
Merge into: lp:juju-gui/experimental
Diff against target: 964 lines (+612/-101)
9 files modified
app/app.js (+209/-30)
app/assets/javascripts/routing.js (+217/-0)
app/modules-debug.js (+4/-0)
app/views/service.js (+6/-5)
bin/merge-files (+1/-0)
test/index.html (+2/-1)
test/test_app.js (+2/-2)
test/test_routing.js (+108/-0)
undocumented (+63/-63)
To merge this branch: bzr merge lp:~bcsaller/juju-gui/namespace-routing
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+148365@code.launchpad.net

Description of the change

Namespace aware routing

Add namespace aware routing to app. This is forward looking. It adds support
for namespaced urls where these are defined as /:ns:/urlFragment/:ns2:/etc.

Each namespace in the url will be preserved and dispatched to accordingly.

Middleware should only fire once for a given request.

https://codereview.appspot.com/7327045/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Reviewers: mp+148365_code.launchpad.net,

Message:
Please take a look.

Description:
Namespace aware routing

Add namespace aware routing to app. This is forward looking. It adds
support
for namespaced urls where these are defined as
/:ns:/urlFragment/:ns2:/etc.

Each namespace in the url will be preserved and dispatched to
accordingly.

Middleware should only fire once for a given request.

https://code.launchpad.net/~bcsaller/juju-gui/namespace-routing/+merge/148365

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/7327045/

Affected files:
   A [revision details]
   M app/app.js
   A app/assets/javascripts/routing.js
   M app/modules-debug.js
   M bin/merge-files
   M test/index.html
   A test/test_routing.js
   M undocumented

373. By Benjamin Saller

merge trunk

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Revision history for this message
Gary Poster (gary) wrote :

I only got halfway through routing.js, but wanted to share what I had
before I went into calls. No one should wait for me: please don't count
me for the two reviews today. However, I will return to this if I can.

Gary

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js
File app/assets/javascripts/routing.js (right):

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode5
app/assets/javascripts/routing.js:5: // particuallary around text
copy and paste error: please update comment to describe what this
actually does (and the old comment has a typo, "particularly," but the
associated module has been removed from trunk).

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode7
app/assets/javascripts/routing.js:7: function _trim(s, char, leading,
trailing) {
I don't understand yet why this only trims a single character. My
suspicion is that, as I read, I'll understand that this is all you need,
and otherwise you would have been in the "oh my gosh JS doesn't have a
built-in regex excape function" place.

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode20
app/assets/javascripts/routing.js:20: function ltrim(s, char) { return
_trim(s, char, true, false); }
Wouldn't it be nice if the built-in trim were flexible enough for what
you need!

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode23
app/assets/javascripts/routing.js:23: function pairs(o) {
docstring would help with easy reading.

Return sorted array of object attribute pairs (name, value).

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode37
app/assets/javascripts/routing.js:37: // Multi dimensional router
(TARDIS).
lol

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode49
app/assets/javascripts/routing.js:49: return parts[0];
trivial, but when placed next to your getQS implementation, "return
url.split('?')[0];" looks pretty reasonable.

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode65
app/assets/javascripts/routing.js:65: var parts =
url.split(this.fragment);
Might be helpful to include examples in comments:

> '/foo/bar'.split(/\/?(:\w+\:)/)
["/foo/bar"]
'/> :baz:/foo/bar'.split(/\/?(:\w+\:)/)
["", ":baz:", "/foo/bar"]

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode67
app/assets/javascripts/routing.js:67: // This is a URL fragment w/o a
namespace
Took me a second to agree, but yes, nice.

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode68
app/assets/javascripts/routing.js:68: result.charmstore = parts[0];
wait a second, that's not general-purpose! :-)

I don't see the need for this to be general purpose. I suggest moving
it into app/routing.js. Alternatively, if you really want to, clean
this up.

https://codereview.appspot.com/7327045/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Made changes around your suggestions, but will wait for other reviews

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js
File app/assets/javascripts/routing.js (right):

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode37
app/assets/javascripts/routing.js:37: // Multi dimensional router
(TARDIS).
On 2013/02/14 14:00:09, gary.poster wrote:
> lol

I forgot I left that in there. :)

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode49
app/assets/javascripts/routing.js:49: return parts[0];
On 2013/02/14 14:00:09, gary.poster wrote:
> trivial, but when placed next to your getQS implementation, "return
> url.split('?')[0];" looks pretty reasonable.

Done.

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode65
app/assets/javascripts/routing.js:65: var parts =
url.split(this.fragment);
On 2013/02/14 14:00:09, gary.poster wrote:
> Might be helpful to include examples in comments:

> > '/foo/bar'.split(/\/?(:\w+\:)/)
> ["/foo/bar"]
> '/> :baz:/foo/bar'.split(/\/?(:\w+\:)/)
> ["", ":baz:", "/foo/bar"]

Done.

https://codereview.appspot.com/7327045/diff/3001/app/assets/javascripts/routing.js#newcode68
app/assets/javascripts/routing.js:68: result.charmstore = parts[0];
On 2013/02/14 14:00:09, gary.poster wrote:
> wait a second, that's not general-purpose! :-)

> I don't see the need for this to be general purpose. I suggest moving
it into
> app/routing.js. Alternatively, if you really want to, clean this up.

Added this.defaultNamespace and use that in place.

https://codereview.appspot.com/7327045/

374. By Benjamin Saller

review feedback

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Revision history for this message
Madison Scott-Clary (makyo) wrote :

Some minors below. The code looks fine for the most part, with the
queue addition. However, I can't get the app to load past the "trying
to connect" screen. I'm seeing deltas in the websocket frames, but
nothing going on in the app. Will try uncommenting logs (so don't get
rid of them yet :) to see what's going on.

https://codereview.appspot.com/7327045/diff/3001/app/app.js
File app/app.js (right):

https://codereview.appspot.com/7327045/diff/3001/app/app.js#newcode346
app/app.js:346: //console.group('dispatch');
Remove commented code.

https://codereview.appspot.com/7327045/diff/3001/app/app.js#newcode381
app/app.js:381: // Null-queue for NS routing. The 1ms delay in the queue
presents
Very minor, should be a /*...*/ style comment - will make it easy to
yuidoc down the road. Or it could just be yuidoc'd, if you want.

https://codereview.appspot.com/7327045/diff/3001/app/app.js#newcode395
app/app.js:395: var loc = Y.getLocation();
Very minor: could be moved to top of function and used in line 391.

https://codereview.appspot.com/7327045/diff/3001/app/app.js#newcode404
app/app.js:404: /**
We should note that this comes from App.Base, along with what has
changed.

https://codereview.appspot.com/7327045/diff/3001/app/app.js#newcode996
app/app.js:996: * dispatches once per any dispatch call wrt namespace
with regards to

https://codereview.appspot.com/7327045/diff/9001/app/assets/javascripts/routing.js
File app/assets/javascripts/routing.js (right):

https://codereview.appspot.com/7327045/diff/9001/app/assets/javascripts/routing.js#newcode48
app/assets/javascripts/routing.js:48: * normalize a url w/o its qs.
Normalize, without

https://codereview.appspot.com/7327045/diff/9001/app/assets/javascripts/routing.js#newcode50
app/assets/javascripts/routing.js:50: * @return {Object} {url: string,
qs: querystring}.
Should be returning just the URL, correct?

https://codereview.appspot.com/7327045/

375. By Benjamin Saller

review changes

Revision history for this message
Benjamin Saller (bcsaller) wrote :

thanks for the review, pushing the updates.

https://codereview.appspot.com/7327045/diff/3001/app/app.js
File app/app.js (right):

https://codereview.appspot.com/7327045/diff/3001/app/app.js#newcode381
app/app.js:381: // Null-queue for NS routing. The 1ms delay in the queue
presents
On 2013/02/14 17:58:27, matthew.scott wrote:
> Very minor, should be a /*...*/ style comment - will make it easy to
yuidoc down
> the road. Or it could just be yuidoc'd, if you want.

Something in me fights doing yuidoc for internal methods and overrides
that should never be called, but I can change it.

https://codereview.appspot.com/7327045/diff/3001/app/app.js#newcode395
app/app.js:395: var loc = Y.getLocation();
On 2013/02/14 17:58:27, matthew.scott wrote:
> Very minor: could be moved to top of function and used in line 391.

Done.

https://codereview.appspot.com/7327045/diff/3001/app/app.js#newcode404
app/app.js:404: /**
On 2013/02/14 17:58:27, matthew.scott wrote:
> We should note that this comes from App.Base, along with what has
changed.

Done.

https://codereview.appspot.com/7327045/diff/9001/app/assets/javascripts/routing.js
File app/assets/javascripts/routing.js (right):

https://codereview.appspot.com/7327045/diff/9001/app/assets/javascripts/routing.js#newcode48
app/assets/javascripts/routing.js:48: * normalize a url w/o its qs.
On 2013/02/14 17:58:27, matthew.scott wrote:
> Normalize, without

Done.

https://codereview.appspot.com/7327045/diff/9001/app/assets/javascripts/routing.js#newcode50
app/assets/javascripts/routing.js:50: * @return {Object} {url: string,
qs: querystring}.
On 2013/02/14 17:58:27, matthew.scott wrote:
> Should be returning just the URL, correct?

Done.

https://codereview.appspot.com/7327045/

Revision history for this message
Benjamin Saller (bcsaller) wrote :
376. By Benjamin Saller

bad merge missed login-mask -> full-screen-mask rename

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Revision history for this message
Benjamin Saller (bcsaller) wrote :

There was an issue with the login-mask renaming that I somehow missed in
the merge, resovled now.

https://codereview.appspot.com/7327045/

Revision history for this message
Gary Poster (gary) wrote :
Download full text (10.6 KiB)

Hi Ben. This is a very impressive branch.

In terms of usability, I think we need a navigation spelling or other
aid to let one dimension easily construct a url that keeps all current
aspects of the url except one. AFAICT, that does not exist. I think we
would need an exposed API call to construct a url only changing a given
namespace + path. Some other convenience functions to use that API
might be nice too (like to navigate within the app).

I'd be happy to talk through this on a call if that would help speed
things up, either to understand my point or to convince me that it is
unnecessary. :-)

I'm not clear on how this prevents re-dispatching along a given
dimension that does not change between two urls--that is,
/:foo:/bar/:bing:/baz -> /:foo:/bar/:bing:/shazam should not redispatch
along the :foo: namespace, if I understand your goals. Again, maybe
that's best for a call, though I might ask for explanatory comments
somewhere afterwards.

Note that I suggest you read my per-line comments through once before
responding item by item--I eventually come to understand things (maybe?)
that I question initially.

Also, please forgive my many comments. This is a meaty branch, and I
decided to let myself selfishly annotate as I went. The annotations
themselves may or may not indicate room for clarity in the code.

Thank you!

Gary

https://codereview.appspot.com/7327045/diff/14001/app/app.js
File app/app.js (right):

https://codereview.appspot.com/7327045/diff/14001/app/app.js#newcode237
app/app.js:237: this._routeGeneration = 0;
This is not a request. It's a mild suggestion.

These four attributes are a bit mysterious on their own. Comments
describing their intent would help the reader along, I think.

For this particular attribute and the next, I see this is needed in
_routeStateTracker and updated elsewhere. Obviously it is some kind of
uid mechanism but I don't understand the details yet as of this writing.

https://codereview.appspot.com/7327045/diff/14001/app/app.js#newcode239
app/app.js:239: this._nsPath = null;
AIUI, this is used to help override _getPath when we are in the parent
class' dispatch, which is run for each namespace.

https://codereview.appspot.com/7327045/diff/14001/app/app.js#newcode240
app/app.js:240: this._nsRouter = juju.Router();
"The router provides the logic to parse a multi-dimensional url into its
composite parts. Each dimension of the url is delineated with namespace
markers, identified by colons before and after the namespace name (e.g.,
:namespace:)."?

https://codereview.appspot.com/7327045/diff/14001/app/app.js#newcode351
app/app.js:351: Y.each(paths, function(p, ns) {
What is ns, and why don't you use it?

https://codereview.appspot.com/7327045/diff/14001/app/app.js#newcode354
app/app.js:354: // to whom it applies.
Thorough thinking.

https://codereview.appspot.com/7327045/diff/14001/app/app.js#newcode365
app/app.js:365: // NS aware getpath
Might as well make it yuidoc linter friendly. It's trivial.

"This is normally internal to the YUI app, and is called to obtain the
url path on which to dispatch. We override it in order to have each
namespaced path handled separately (see teh dispatch method a...

Revision history for this message
Jeff Pihach (hatch) wrote :

This looks great! I just have a couple small comments :-)

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js
File app/assets/javascripts/routing.js (right):

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js#newcode5
app/assets/javascripts/routing.js:5: function _trim(s, char, leading,
trailing) {
This feels like a generic utility method which could be split out into a
separate utility method module and hung off Y.Lang so that it's
available application wide.

As a generic utility method it would probably be best solved using a
regex. Ln 5-18 could be replaced with:

** WARNING - incoming untested code **

Y.Lang.trimString = function(str, start, end) {

   var trimRegex = new RegExp("^[" + start + "]+|[" + end + "]+$","g");
   return str.replace(trimRegex, "");

}

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js#newcode90
app/assets/javascripts/routing.js:90: console.log('URL namespace without
path');
If you're creating logs for debugging it's best to use Y.log() because
it can then be filtered in or out during debugging instead of always
logging to the console.

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js#newcode96
app/assets/javascripts/routing.js:96: console.log('URL has more than one
refernce to same namespace');
s/refernce/reference :-) also see above comment about Y.log()

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js#newcode113
app/assets/javascripts/routing.js:113: var u = '/';
u is url? Please don't manually minify - use meaningful variable names
</pet peeve> :-)

https://codereview.appspot.com/7327045/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

Just a quick FYI.

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js
File app/assets/javascripts/routing.js (right):

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js#newcode90
app/assets/javascripts/routing.js:90: console.log('URL namespace without
path');
On 2013/02/18 17:12:47, jeff.pihach wrote:
> If you're creating logs for debugging it's best to use Y.log() because
it can
> then be filtered in or out during debugging instead of always logging
to the
> console.

For what it's worth, console is current filtered by deployment path
(i.e.: turned off for production, etc). The reasons for using
console.log() also include the fact that one can log multiple objects,
group, and nest logs. This is part of our coding standard decided on
previously.

https://codereview.appspot.com/7327045/

Revision history for this message
Jeff Pihach (hatch) wrote :

> Building regexes programmatically requires escaping the inputs.

> For example, what if I want to trim any leading "bar" off a string and
I pass in
> "bar" as the start parameter and "rabid" as the str. Instead of
getting "rabid"
> back (because the prefix wasn't there to get removed) I instead get
back "id"
> because my start string was interpreted as a set of characters not a
literal
> string.

Agreed, sorry I just wrote that method into the comment box - it wasn't
meant to be a copy/paste solution :-)

https://codereview.appspot.com/7327045/

Revision history for this message
Gary Poster (gary) wrote :

Data:

* Trim is available in recent JS, but it only trims whitespace:
https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/Trim

* JS does not have a regex escape function, but YUI does:
http://yuilibrary.com/yui/docs/api/classes/Escape.html

* To my knowledge YUI does not implement an arbitrary trim function,
such as the one Ben would need.

Opinions:

* I agree with Benji that we don't want to mutate YUI modules.

* I think Ben's current one-off functions are fine for now, as I said in
my review. We could add trim function to a module somewhere as a slack
task if someone wanted to, or if Ben wanted to. I do agree that using
the regex might in fact lead to somewhat more concise code, but,
<shrug>.

I'd like to see this landed asap.

https://codereview.appspot.com/7327045/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

On 2013/02/15 18:19:40, gary.poster wrote:

> In terms of usability, I think we need a navigation spelling or other
aid to let
> one dimension easily construct a url that keeps all current aspects of
the url
> except one. AFAICT, that does not exist. I think we would need an
exposed API
> call to construct a url only changing a given namespace + path. Some
other
> convenience functions to use that API might be nice too (like to
navigate within
> the app).

Thank you for this very through review. I didn't properly respond when
you wrote it as I was in the thick of dealing with the core issues you
raised rather than the finer points. I think I've addressed all the
major issues.

There is one outstanding option (that I haven't taken) at the higher
level api. Routes are dispatched currently w/o additional filtering on a
fixed namespace. By this I mean that /foo should match in one and only
one namespace (even though it will be preserved in the URL in the
namespace it was found in). We may want to change this by requiring
routes to belong to a SubApp with a fixed namespace or by having a
namespace attribute in the route and changing App.match to include this.

There are a few other minor ramifications of this branch, the most
noteworth is that URLs should end in a slash '/'. This allows for
consistent addition of other namespaces to the URL and simplifies
parsing. Where possible views should ask the application of the reverse
URL mapping using app.getModelURL. Changes of this nature were made in
the codebase as well.

I expect that we will hit a remaining issue or two but that this will
not happen till we being using this branch, I think its ready for that.
Because there is a reasonable delta in the branch (the change to all the
code living in _dispatch/_navigate) I will resubmit but it should be
familiar to anyone who has reviewed this branch.

https://codereview.appspot.com/7327045/

377. By Benjamin Saller

move all logic into _dispatch/_navigate, this simplifies the codebase, juju.Router can parse URL more like Location object, more testing

378. By Benjamin Saller

enable all tests and update paths

379. By Benjamin Saller

review changes

380. By Benjamin Saller

merge trunk

381. By Benjamin Saller

lint

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks, I've included the minors, I didn't change to the regex because
of the escaping. If someone else needs this sort of method I'd be happy
to improve those for reuse but we made it pretty far w/o.

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js
File app/assets/javascripts/routing.js (right):

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js#newcode5
app/assets/javascripts/routing.js:5: function _trim(s, char, leading,
trailing) {
On 2013/02/18 20:00:06, benji wrote:
> On 2013/02/18 17:12:47, jeff.pihach wrote:
> > This feels like a generic utility method which could be split out
into a
> > separate utility method module

> +1 I'm surprised trimming strings isn't already available in JS or
YUI.

> > and hung off Y.Lang so that it's available application wide.

> -1 to injecting ourselves into a third-party library

> > As a generic utility method it would probably be best solved using a
regex. Ln
> > 5-18 could be replaced with:
> >
> > ** WARNING - incoming untested code **
> >
> > Y.Lang.trimString = function(str, start, end) {
> >
> > var trimRegex = new RegExp("^[" + start + "]+|[" + end +
"]+$","g");
> > return str.replace(trimRegex, "");
> >
> > }

> Building regexes programmatically requires escaping the inputs.

> For example, what if I want to trim any leading "bar" off a string and
I pass in
> "bar" as the start parameter and "rabid" as the str. Instead of
getting "rabid"
> back (because the prefix wasn't there to get removed) I instead get
back "id"
> because my start string was interpreted as a set of characters not a
literal
> string.

Done.

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js#newcode96
app/assets/javascripts/routing.js:96: console.log('URL has more than one
refernce to same namespace');
On 2013/02/18 17:12:47, jeff.pihach wrote:
> s/refernce/reference :-) also see above comment about Y.log()

Done.

https://codereview.appspot.com/7327045/diff/16001/app/assets/javascripts/routing.js#newcode113
app/assets/javascripts/routing.js:113: var u = '/';
On 2013/02/18 17:12:47, jeff.pihach wrote:
> u is url? Please don't manually minify - use meaningful variable names
</pet
> peeve> :-)

Done.

https://codereview.appspot.com/7327045/

Revision history for this message
Benjamin Saller (bcsaller) wrote :
Revision history for this message
Jeff Pihach (hatch) wrote :

Looks good! I just wanted to add a comment for a future enhancement.

https://codereview.appspot.com/7327045/diff/26001/app/app.js
File app/app.js (right):

https://codereview.appspot.com/7327045/diff/26001/app/app.js#newcode351
app/app.js:351: _navigate: function(url, options) {
I would like to see this in a YUI extension eventually. Extensions
encourage modularity, make it a lot easier to document and, for external
developers, to see that you're overwriting core methods. This would also
follow the 'YUI way' for this type of modification. - We can do this in
a separate branch later -

https://codereview.appspot.com/7327045/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

Land as is - looks good to me and works well. Thanks for the branch!

https://codereview.appspot.com/7327045/

Revision history for this message
Benjamin Saller (bcsaller) wrote :

*** Submitted:

Namespace aware routing

Add namespace aware routing to app. This is forward looking. It adds
support
for namespaced urls where these are defined as
/:ns:/urlFragment/:ns2:/etc.

Each namespace in the url will be preserved and dispatched to
accordingly.

Middleware should only fire once for a given request.

R=gary.poster, matthew.scott, jeff.pihach, benji
CC=
https://codereview.appspot.com/7327045

https://codereview.appspot.com/7327045/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'app/app.js'
--- app/app.js 2013-02-15 11:58:05 +0000
+++ app/app.js 2013-02-20 04:24:21 +0000
@@ -232,6 +232,13 @@
232 consoleManager.noop();232 consoleManager.noop();
233 }233 }
234 }234 }
235
236 // Namespaced URL tracker.
237 this._routeGeneration = 0;
238 this._routeStates = {};
239 this._nsRouter = juju.Router('charmstore');
240
241
235 // Create a client side database to store state.242 // Create a client side database to store state.
236 this.db = new models.Database();243 this.db = new models.Database();
237 this.serviceEndpoints = {};244 this.serviceEndpoints = {};
@@ -336,6 +343,136 @@
336 },343 },
337344
338 /**345 /**
346 * NS aware navigate wrapper. This has the feature
347 * of preserving existing namespaces in the url.
348 *
349 * @method _navigate
350 **/
351 _navigate: function(url, options) {
352 var loc = Y.getLocation();
353 var qs = this._nsRouter.getQS(url);
354 var result = this._nsRouter.combine(loc.pathname, url);
355 if (qs) {
356 result += '?' + qs;
357 }
358 if (JujuGUI.superclass._navigate.call(this, url, options)) {
359 // Queue/Save the entire URL, not just the new fragment.
360 this._queue(result, true);
361 return true;
362 }
363 return false;
364 },
365
366
367 /**
368 * Null-queue for NS routing. The 1ms delay in the queue presents problems,
369 * apply url saves as they happen.
370 *
371 * Overrides superclass, formalizes dependency on HTML5 paths.
372 * @method _queue
373 **/
374 _queue: function() {
375 // Sync Invocation
376 this._save.apply(this, arguments);
377 },
378
379 /**
380 Dispatches to the first route handler that matches the specified _path_.
381
382 If called before the `ready` event has fired, the dispatch will be aborted.
383 This ensures normalized behavior between Chrome (which fires a `popstate`
384 event on every pageview) and other browsers (which do not).
385
386 @method _dispatch
387 @param {String} path URL path.
388 @param {String} url Full URL.
389 @param {String} src What initiated the dispatch.
390 @chainable
391 @protected
392 **/
393 _dispatch: function(path, url, src) {
394 var self = this,
395 routes,
396 callbacks = [],
397 namespaces = [],
398 matches, req, res, parts;
399
400 self._dispatching = self._dispatched = true;
401
402 parts = this._nsRouter.split(path);
403 namespaces = this._nsRouter.parse(parts.pathname);
404 this._routeGeneration += 1;
405
406 Y.each(namespaces, function(fragment, namespace) {
407 routes = self.match(fragment);
408 if (!routes || !routes.length) {
409 self._dispatching = false;
410 return self;
411 }
412
413 //console.log("_dispatch", fragment, url, src);
414 req = self._getRequest(fragment, url, src);
415 res = self._getResponse(req);
416
417 req.next = function(err) {
418 var callback, route;
419
420 if (err) {
421 // Special case "route" to skip to the next route handler
422 // avoiding any additional callbacks for the current route.
423 if (err === 'route') {
424 callbacks = [];
425 req.next();
426 } else {
427 Y.error(err);
428 }
429
430 } else if ((callback = callbacks.shift())) {
431 //console.group('callback');
432 //console.log('callback', callback);
433 if (typeof callback === 'string') {
434 callback = self[callback];
435 }
436
437 // Allow access to the num or remaining callbacks for the route.
438 req.pendingCallbacks = callbacks.length;
439 // Attach the callback id to the request.
440 req.callbackId = Y.stamp(callback, true);
441 callback.call(self, req, res, req.next);
442 //console.groupEnd();
443
444 } else if ((route = routes.shift())) {
445 // Make a copy of this route's `callbacks` and find its matches.
446 //console.group("route", fragment, route.path)
447 callbacks = route.callbacks.concat();
448 matches = route.regex.exec(fragment);
449
450 // Use named keys for parameter names if the route path contains
451 // named keys. Otherwise, use numerical match indices.
452 if (matches.length === route.keys.length + 1) {
453 req.params = Y.Array.hash(route.keys, matches.slice(1));
454 } else {
455 req.params = matches.concat();
456 }
457
458 // Allow access tot he num of remaining routes for this request.
459 req.pendingRoutes = routes.length;
460
461 // Execute this route's `callbacks`.
462 req.next();
463 //console.groupEnd();
464 }
465 };
466
467 req.next();
468 });
469
470
471 self._dispatching = false;
472 return self._dequeue();
473 },
474
475 /**
339 * Hook up all of the declared behaviors.476 * Hook up all of the declared behaviors.
340 *477 *
341 * @method enableBehaviors478 * @method enableBehaviors
@@ -466,9 +603,6 @@
466 * @private603 * @private
467 */604 */
468 _buildServiceView: function(req, viewName) {605 _buildServiceView: function(req, viewName) {
469 console.log('App: Route: Service',
470 viewName, req.params.id, req.path, req.pendingRoutes);
471
472 var service = this.db.services.getById(req.params.id);606 var service = this.db.services.getById(req.params.id);
473 this._prefetch_service(service);607 this._prefetch_service(service);
474 this.showView(viewName, {608 this.showView(viewName, {
@@ -518,7 +652,6 @@
518 * @method show_charm_collection652 * @method show_charm_collection
519 */653 */
520 show_charm_collection: function(req) {654 show_charm_collection: function(req) {
521 console.log('App: Route: Charm Collection', req.path, req.query);
522 this.showView('charm_collection', {655 this.showView('charm_collection', {
523 query: req.query.q,656 query: req.query.q,
524 charm_store: this.charm_store657 charm_store: this.charm_store
@@ -529,7 +662,6 @@
529 * @method show_charm662 * @method show_charm
530 */663 */
531 show_charm: function(req) {664 show_charm: function(req) {
532 console.log('App: Route: Charm', req.path, req.params);
533 var charm_url = req.params.charm_store_path;665 var charm_url = req.params.charm_store_path;
534 this.showView('charm', {666 this.showView('charm', {
535 charm_data_url: charm_url,667 charm_data_url: charm_url,
@@ -644,9 +776,14 @@
644 * @private776 * @private
645 */777 */
646 onLogin: function() {778 onLogin: function() {
647 Y.one('body > #full-screen-mask').hide();779 var mask = Y.one('#full-screen-mask');
648 // Stop the animated loading spinner.780 if (mask) {
649 spinner.stop();781 mask.hide();
782 // Stop the animated loading spinner.
783 if (spinner) {
784 spinner.stop();
785 }
786 }
650 this.dispatch();787 this.dispatch();
651 },788 },
652789
@@ -776,12 +913,13 @@
776913
777 // Replace the path params with items from the model's attributes.914 // Replace the path params with items from the model's attributes.
778 path = path.replace(regexPathParam,915 path = path.replace(regexPathParam,
779 function(match, operator, key) {916 function(match, operator, key) {
780 if (reverse_map !== undefined && reverse_map[key]) {917 if (reverse_map !== undefined &&
781 key = reverse_map[key];918 reverse_map[key]) {
782 }919 key = reverse_map[key];
783 return attrs[key];920 }
784 });921 return attrs[key];
922 });
785 matches.push(Y.mix({path: path,923 matches.push(Y.mix({path: path,
786 route: route,924 route: route,
787 attrs: attrs,925 attrs: attrs,
@@ -814,12 +952,51 @@
814 Y.Array.each(routes, function(route) {952 Y.Array.each(routes, function(route) {
815 // Additionally pass route as options. This is needed to pass through953 // Additionally pass route as options. This is needed to pass through
816 // the attribute setter.954 // the attribute setter.
817 this.route(route.path, route.callback, route);955 // Callback can be an array, we push a state tracker to the head of
956 // each callback chain.
957 var callbacks = route.callbacks || route.callback;
958 if (!Y.Lang.isArray(callbacks)) {
959 callbacks = [callbacks];
960 }
961 // Tag each callback such that we can resolve it in
962 // the state tracker.
963 Y.Array.each(callbacks, function(cb) {Y.stamp(cb);});
964 // Inject our state tracker
965 if (callbacks[0] !== '_routeStateTracker') {
966 callbacks.unshift('_routeStateTracker');
967 }
968 route.callbacks = callbacks.concat();
969 // Additionally pass the route with its exteneded
970 // attribute set.
971 this.route(route.path, route.callbacks, route);
818 }, this);972 }, this);
819 return this._routes.concat();973 return this._routes.concat();
820 },974 },
821975
822 /**976 /**
977 * Internal state tracker, makes sure a given route
978 * dispatches once per any dispatch call with regard to
979 * namespace components.
980 *
981 * @method _routeStateTracker
982 **/
983 _routeStateTracker: function(req, res, next) {
984 var gen = this._routeGeneration,
985 seen = this._routeStates,
986 callbackId = req.callbackId;
987
988 if (callbackId && seen[callbackId] && (seen[callbackId] >= gen)) {
989 // Calling next with a route error aborts
990 // further callbacks in _this_ array (remember
991 // route.callbacks is an array per route).
992 // But can allow continued processing;
993 next('route');
994 }
995 next();
996 seen[callbackId] = gen;
997 },
998
999 /**
823 * Wrap the default routing using a whitelist to avoid extra juggling.1000 * Wrap the default routing using a whitelist to avoid extra juggling.
824 *1001 *
825 * @method route1002 * @method route
@@ -843,6 +1020,7 @@
8431020
844 }, {1021 }, {
845 ATTRS: {1022 ATTRS: {
1023 html5: true,
846 charm_store: {},1024 charm_store: {},
8471025
848 /*1026 /*
@@ -859,39 +1037,39 @@
859 routes: {1037 routes: {
860 value: [1038 value: [
861 // Called on each request.1039 // Called on each request.
862 { path: '*', callback: 'check_user_credentials'},1040 { path: '*', callbacks: 'check_user_credentials'},
863 { path: '*', callback: 'show_notifications_view'},1041 { path: '*', callbacks: 'show_notifications_view'},
864 // Charms.1042 // Charms.
865 { path: '/charms/', callback: 'show_charm_collection'},1043 { path: '/charms/', callbacks: 'show_charm_collection'},
866 { path: '/charms/*charm_store_path',1044 { path: '/charms/*charm_store_path/',
867 callback: 'show_charm',1045 callbacks: 'show_charm',
868 model: 'charm'},1046 model: 'charm'},
869 // Notifications.1047 // Notifications.
870 { path: '/notifications/',1048 { path: '/notifications/',
871 callback: 'show_notifications_overview'},1049 callbacks: 'show_notifications_overview'},
872 // Services.1050 // Services.
873 { path: '/service/:id/config',1051 { path: '/service/:id/config/',
874 callback: 'show_service_config',1052 callbacks: 'show_service_config',
875 intent: 'config',1053 intent: 'config',
876 model: 'service'},1054 model: 'service'},
877 { path: '/service/:id/constraints',1055 { path: '/service/:id/constraints/',
878 callback: 'show_service_constraints',1056 callbacks: 'show_service_constraints',
879 intent: 'constraints',1057 intent: 'constraints',
880 model: 'service'},1058 model: 'service'},
881 { path: '/service/:id/relations',1059 { path: '/service/:id/relations/',
882 callback: 'show_service_relations',1060 callbacks: 'show_service_relations',
883 intent: 'relations',1061 intent: 'relations',
884 model: 'service'},1062 model: 'service'},
885 { path: '/service/:id/',1063 { path: '/service/:id/',
886 callback: 'show_service',1064 callbacks: 'show_service',
887 model: 'service'},1065 model: 'service'},
888 // Units.1066 // Units.
889 { path: '/unit/:id/',1067 { path: '/unit/:id/',
890 callback: 'show_unit',1068 callbacks: 'show_unit',
891 reverse_map: {id: 'urlName'},1069 reverse_map: {id: 'urlName'},
892 model: 'serviceUnit'},1070 model: 'serviceUnit'},
893 // Root.1071 // Root.
894 { path: '/', callback: 'show_environment'}1072 { path: '/', callbacks: 'show_environment'}
895 ]1073 ]
896 }1074 }
897 }1075 }
@@ -906,6 +1084,7 @@
906 'juju-charm-store',1084 'juju-charm-store',
907 'juju-models',1085 'juju-models',
908 'juju-notifications',1086 'juju-notifications',
1087 'juju-routing',
909 // This alias does not seem to work, including references by hand.1088 // This alias does not seem to work, including references by hand.
910 'juju-controllers',1089 'juju-controllers',
911 'juju-notification-controller',1090 'juju-notification-controller',
9121091
=== added file 'app/assets/javascripts/routing.js'
--- app/assets/javascripts/routing.js 1970-01-01 00:00:00 +0000
+++ app/assets/javascripts/routing.js 2013-02-20 04:24:21 +0000
@@ -0,0 +1,217 @@
1'use strict';
2
3YUI.add('juju-routing', function(Y) {
4
5 function _trim(s, char, leading, trailing) {
6 // remove leading, trailing char.
7 while (leading && s && s.indexOf(char) === 0) {
8 s = s.slice(1, s.length);
9 }
10 while (trailing && s && s.lastIndexOf(char) === (s.length - 1)) {
11 s = s.slice(0, s.length - 1);
12 }
13 return s;
14 }
15
16 function trim(s, char) { return _trim(s, char, true, true); }
17 function rtrim(s, char) { return _trim(s, char, false, true); }
18 function ltrim(s, char) { return _trim(s, char, true, false); }
19
20
21 /**
22 * Return a sorted array of namespace, url pairs.
23 *
24 * @method pairs
25 * @return {Object} [[namespace, url]].
26 **/
27 function pairs(o) {
28 var result = [],
29 keys = Y.Object.keys(o).sort();
30
31 Y.each(keys, function(k) {
32 result.push([k, o[k]]);
33 });
34 return result;
35 }
36
37 var Routes = {
38 pairs: function() {return pairs(this);}
39 };
40
41 // Multi dimensional router (TARDIS).
42 var _Router = {
43 // Regex to parse namespace, url fragment pairs.
44 _fragment: /\/?(:\w+\:)/,
45 _regexUrlOrigin: /^(?:[^\/#?:]+:\/\/|\/\/)[^\/]*/,
46 defaultNamespace: 'default',
47
48 /**
49 * Return the query string portion of a URL.
50 * @method getQS
51 **/
52 getQS: function(url) {
53 return url.split('?')[1];
54 },
55
56 /**
57 * Split a URL into components, a subset of the
58 * Location Object.
59 *
60 * @method split
61 * @param {String} url to split.
62 * @return {Object} hash of URL parts.
63 **/
64 split: function(url) {
65 var result = {
66 href: url
67 };
68 var origin = this._regexUrlOrigin.exec(url);
69 result.search = this.getQS(url);
70
71 if (origin) {
72 // Take the match.
73 result.origin = origin = origin[0];
74 // And remove it from the url.
75 result.pathname = url.substr(origin.length);
76 } else {
77 result.pathname = url;
78 }
79
80 if (result.search) {
81 result.pathname = result.pathname.substr(0,
82 (result.pathname.length - result.search.length) - 1);
83 }
84
85 return result;
86 },
87
88 /**
89 * Parse a url into an Object with namespaced url fragments for values.
90 * Each value will be normalized to include a trailing slash
91 * ('/').
92 *
93 * @method parse
94 * @param {String} url to parse.
95 * @return {Object} result is {ns: url fragment {String}}.
96 **/
97 parse: function(url) {
98 var result = Object.create(Routes, {
99 defaultNamespacePresent: {
100 enumerable: false,
101 writable: true
102 }
103 });
104 var parts = this.split(url);
105 url = parts.pathname;
106
107 parts = url.split(this._fragment);
108 // > '/foo/bar'.split(this._fragment)
109 // ["/foo/bar"]
110 // > :baz:/foo/bar'.split(this._fragment)
111 // ["", ":baz:", "/foo/bar"]
112 if (parts[0]) {
113 // This is a URL fragment without a namespace.
114 parts[0] = rtrim(parts[0], '/') + '/';
115 result[this.defaultNamespace] = parts[0];
116 result.defaultNamespacePresent = true;
117 } else {
118 result[this.defaultNamespace] = '/'; // A sane default.
119 }
120 // Now scan each pair after [0] for ns/route elements
121 for (var i = 1; i < parts.length; i += 2) {
122 var ns = trim(parts[i], ':'),
123 val = '/';
124
125 if ((i + 1) > parts.length) {
126 console.log('URL namespace without path');
127 } else {
128 val = parts[i + 1];
129 }
130
131 if (result[ns] !== undefined) {
132 console.log('URL has more than one reference to same namespace');
133 }
134 if (ns === this.defaultNamespace) {
135 result.defaultNamespacePresent = true;
136 }
137 result[ns] = rtrim(val, '/') + '/';
138 }
139 return result;
140 },
141
142 /**
143 * Given an object with ns:url_fragment pairs
144 * produce a properly ordered url.
145 *
146 * @method url
147 * @param {Object} componets is the result of a parse(url) call.
148 * @return {String} url.
149 **/
150 url: function(components) {
151 var base = Y.mix({}, components);
152 var url = '/';
153
154 function slash(u) {
155 if (u.lastIndexOf('/') !== u.length - 1) {
156 u += '/';
157 }
158 return u;
159 }
160
161 if (base[this.defaultNamespace]) {
162 url += trim(base[this.defaultNamespace], '/');
163 delete base[this.defaultNamespace];
164 }
165
166 // Sort base properties such
167 // that output ordering is uniform.
168 var keys = Y.Object.keys(base).sort();
169 Y.each(keys, function(ns) {
170 url = slash(url);
171 url += ':' + ns + ':' + base[ns];
172 });
173
174 url = slash(url);
175 return url;
176 },
177
178 /**
179 * Smartly combine new namespaced url components with old.
180 *
181 * @method combine
182 * @param {Object} orig url.
183 * @param {Object} incoming new url.
184 * @return {String} a new namespaced url.
185 **/
186 combine: function(orig, incoming) {
187 var url;
188
189 if (Y.Lang.isString(orig)) {
190 orig = this.parse(orig);
191 }
192 if (Y.Lang.isString(incoming)) {
193 incoming = this.parse(incoming);
194 }
195
196 if (!incoming.defaultNamespacePresent) {
197 // The default namespace was supplied (rather
198 // than defaulting to /) in the incoming url,
199 // this means we can safely override it.
200 delete incoming[this.defaultNamespace];
201 }
202 url = this.url(Y.mix(orig, incoming, true, Y.Object.keys(incoming)));
203 return url;
204
205 }
206 };
207
208 Y.namespace('juju').Router = function(defaultNamespace) {
209 var r = Object.create(_Router);
210 r.defaultNamespace = defaultNamespace;
211 return r;
212 };
213
214
215}, '0.1.0', {
216 requires: ['oop']
217});
0218
=== modified file 'app/modules-debug.js'
--- app/modules-debug.js 2013-02-15 11:58:05 +0000
+++ app/modules-debug.js 2013-02-20 04:24:21 +0000
@@ -52,6 +52,10 @@
52 fullpath: '/juju-ui/assets/javascripts/reconnecting-websocket.js'52 fullpath: '/juju-ui/assets/javascripts/reconnecting-websocket.js'
53 },53 },
5454
55 'juju-routing': {
56 fullpath: '/juju-ui/assets/javascripts/routing.js'
57 },
58
55 // Views59 // Views
56 'juju-topology-relation': {60 'juju-topology-relation': {
57 fullpath: '/juju-ui/views/topology/relation.js'61 fullpath: '/juju-ui/views/topology/relation.js'
5862
=== modified file 'app/views/service.js'
--- app/views/service.js 2013-02-08 17:36:58 +0000
+++ app/views/service.js 2013-02-20 04:24:21 +0000
@@ -315,20 +315,21 @@
315 getServiceTabs: function(href) {315 getServiceTabs: function(href) {
316 var db = this.get('db'),316 var db = this.get('db'),
317 service = this.get('model'),317 service = this.get('model'),
318 getModelURL = this.get('getModelURL'),
318 charmId = service.get('charm'),319 charmId = service.get('charm'),
319 charm = db.charms.getById(charmId),320 charm = db.charms.getById(charmId),
320 charmUrl = (charm ? this.get('getModelURL')(charm) : '#');321 charmUrl = (charm ? getModelURL(charm) : '#');
321322
322 var tabs = [{323 var tabs = [{
323 href: '.',324 href: getModelURL(service),
324 title: 'Units',325 title: 'Units',
325 active: false326 active: false
326 }, {327 }, {
327 href: 'relations',328 href: getModelURL(service, 'relations'),
328 title: 'Relations',329 title: 'Relations',
329 active: false330 active: false
330 }, {331 }, {
331 href: 'config',332 href: getModelURL(service, 'config'),
332 title: 'Settings',333 title: 'Settings',
333 active: false334 active: false
334 }, {335 }, {
@@ -336,7 +337,7 @@
336 title: 'Charm',337 title: 'Charm',
337 active: false338 active: false
338 }, {339 }, {
339 href: 'constraints',340 href: getModelURL(service, 'constraints'),
340 title: 'Constraints',341 title: 'Constraints',
341 active: false342 active: false
342 }];343 }];
343344
=== modified file 'bin/merge-files'
--- bin/merge-files 2013-02-13 18:28:30 +0000
+++ bin/merge-files 2013-02-20 04:24:21 +0000
@@ -72,6 +72,7 @@
72 'app/assets/javascripts/d3.v2.min.js',72 'app/assets/javascripts/d3.v2.min.js',
73 'app/assets/javascripts/d3-components.js',73 'app/assets/javascripts/d3-components.js',
74 'app/assets/javascripts/reconnecting-websocket.js',74 'app/assets/javascripts/reconnecting-websocket.js',
75 'app/assets/javascripts/routing.js',
75 'app/assets/javascripts/gallery-ellipsis.js',76 'app/assets/javascripts/gallery-ellipsis.js',
76 'app/assets/javascripts/gallery-markdown.js',77 'app/assets/javascripts/gallery-markdown.js',
77 'app/assets/javascripts/gallery-timer.js']);78 'app/assets/javascripts/gallery-timer.js']);
7879
=== modified file 'test/index.html'
--- test/index.html 2013-02-15 12:29:14 +0000
+++ test/index.html 2013-02-20 04:24:21 +0000
@@ -29,6 +29,7 @@
2929
3030
31 <!-- Tests (Alphabetical)-->31 <!-- Tests (Alphabetical)-->
32
32 <script src="test_app.js"></script>33 <script src="test_app.js"></script>
33 <script src="test_app_hotkeys.js"></script>34 <script src="test_app_hotkeys.js"></script>
34 <script src="test_application_notifications.js"></script>35 <script src="test_application_notifications.js"></script>
@@ -48,6 +49,7 @@
48 <script src="test_notifications.js"></script>49 <script src="test_notifications.js"></script>
49 <script src="test_notifier_widget.js"></script>50 <script src="test_notifier_widget.js"></script>
50 <script src="test_panzoom.js"></script>51 <script src="test_panzoom.js"></script>
52 <script src="test_routing.js"></script>
51 <script src="test_service_config_view.js"></script>53 <script src="test_service_config_view.js"></script>
52 <script src="test_service_module.js"></script>54 <script src="test_service_module.js"></script>
53 <script src="test_service_view.js"></script>55 <script src="test_service_view.js"></script>
@@ -58,7 +60,6 @@
58 <script src="test_unit_view.js"></script>60 <script src="test_unit_view.js"></script>
59 <script src="test_utils.js"></script>61 <script src="test_utils.js"></script>
60 <script src="test_viewport_module.js"></script>62 <script src="test_viewport_module.js"></script>
61
62 <script>63 <script>
63 YUI_config = {64 YUI_config = {
64 async: false,65 async: false,
6566
=== modified file 'test/test_app.js'
--- test/test_app.js 2013-02-15 11:58:05 +0000
+++ test/test_app.js 2013-02-20 04:24:21 +0000
@@ -113,14 +113,14 @@
113 app.getModelURL(wordpress).should.equal('/service/wordpress/');113 app.getModelURL(wordpress).should.equal('/service/wordpress/');
114 // However, passing 'intent' can force selection of another one.114 // However, passing 'intent' can force selection of another one.
115 app.getModelURL(wordpress, 'config').should.equal(115 app.getModelURL(wordpress, 'config').should.equal(
116 '/service/wordpress/config');116 '/service/wordpress/config/');
117117
118 // Service units use argument rewriting (thus not /u/wp/0).118 // Service units use argument rewriting (thus not /u/wp/0).
119 app.getModelURL(wp0).should.equal('/unit/wordpress-0/');119 app.getModelURL(wp0).should.equal('/unit/wordpress-0/');
120120
121 // Charms also require a mapping, but only a name, not a function.121 // Charms also require a mapping, but only a name, not a function.
122 app.getModelURL(wp_charm).should.equal(122 app.getModelURL(wp_charm).should.equal(
123 '/charms/charms/precise/wordpress-6/json');123 '/charms/charms/precise/wordpress-6/json/');
124 });124 });
125125
126 it('should display the configured environment name', function() {126 it('should display the configured environment name', function() {
127127
=== added file 'test/test_routing.js'
--- test/test_routing.js 1970-01-01 00:00:00 +0000
+++ test/test_routing.js 2013-02-20 04:24:21 +0000
@@ -0,0 +1,108 @@
1
2'use strict';
3
4describe('Namespaced Routing', function() {
5 var Y, juju, app;
6
7 before(function(done) {
8 Y = YUI(GlobalConfig).use(['juju-routing',
9 'juju-gui'],
10 function(Y) {
11 juju = Y.namespace('juju');
12 done();
13 });
14 });
15
16 beforeEach(function() {
17 app = new juju.App();
18 });
19
20 it('should support basic namespaced urls', function() {
21 var router = juju.Router('charmstore');
22
23 var match = router.parse('/');
24 assert(match.charmstore === '/');
25 assert(match.inspector === undefined);
26
27 match = router.parse('cs:mysql');
28 assert(match.charmstore === 'cs:mysql/');
29 assert(match.inspector === undefined);
30
31 match = router.parse(
32 '/:inspector:/services/mysql/:charmstore:/charms/precise/mediawiki');
33 assert(match.charmstore === '/charms/precise/mediawiki/');
34 assert(match.inspector === '/services/mysql/');
35
36 match.pairs().should.eql(
37 [['charmstore', '/charms/precise/mediawiki/'],
38 ['inspector', '/services/mysql/']]);
39
40 match = router.parse(
41 '/charms/precise/mediawiki/:inspector:/services/mysql/');
42 assert(match.charmstore === '/charms/precise/mediawiki/');
43 assert(match.inspector === '/services/mysql/');
44
45 var u = router.url(match);
46 assert(u === '/charms/precise/mediawiki/:inspector:/services/mysql/');
47
48 // Sorted keys.
49 u = router.url({charmstore: '/', gamma: 'g', a: 'alpha', b: 'beta'});
50 assert(u === '/:a:alpha/:b:beta/:gamma:g/');
51
52 // Sorted keys with actual charmstore [defaultNamespace] component.
53 u = router.url({
54 charmstore: '/charms/precise/mysql',
55 gamma: 'g', a: 'alpha', b: 'beta'});
56 assert(u === '/charms/precise/mysql/:a:alpha/:b:beta/:gamma:g/');
57
58 u = router.url({gamma: 'g', a: 'alpha', b: 'beta'});
59 assert(u === '/:a:alpha/:b:beta/:gamma:g/');
60 });
61
62
63 it('should support a default namespace', function() {
64 var router = juju.Router('charmstore');
65 var url, parts;
66 router.defaultNamespace.should.equal('charmstore');
67
68 // Use a different namespace.
69 router = juju.Router('foo');
70 parts = router.parse('/bar');
71 url = router.url(parts);
72 url.should.equal('/bar/');
73
74 // .. and with an additional ns.
75 parts.extra = 'happens';
76 url = router.url(parts);
77 url.should.equal('/bar/:extra:happens/');
78 });
79
80
81 it('should be able to cleanly combine urls preserving untouched namespaces',
82 function() {
83 var router = juju.Router('charmstore');
84 var url, parts;
85 url = router.combine('/foo/bar', '/:inspector:/foo/');
86 url.should.equal('/foo/bar/:inspector:/foo/');
87 });
88
89
90 it('should be able to split qualified urls', function() {
91 var router = juju.Router('playground');
92 var url, parts;
93
94 parts = router.split('http://foo.bar:8888/foo/bar');
95 parts.pathname.should.equal('/foo/bar');
96 parts.origin.should.equal('http://foo.bar:8888');
97
98 parts = router.split('http://foo.bar:8888/foo/bar/?a=b');
99 parts.pathname.should.equal('/foo/bar/');
100 parts.origin.should.equal('http://foo.bar:8888');
101 parts.search.should.equal('a=b');
102
103 parts = router.split('/foo/bar/?a=b');
104 parts.pathname.should.equal('/foo/bar/');
105 parts.search.should.equal('a=b');
106 });
107
108});
0109
=== modified file 'undocumented'
--- undocumented 2013-02-18 09:42:16 +0000
+++ undocumented 2013-02-20 04:24:21 +0000
@@ -1,33 +1,3 @@
1app/models/charm.js:117 "parse"
2app/models/charm.js:134 "compare"
3app/models/charm.js:108 "failure"
4app/models/charm.js:80 "sync"
5app/models/charm.js:50 "initializer"
6app/models/charm.js:160 "validator"
7app/models/models.js:179 "update_service_unit_aggregates"
8app/models/models.js:445 "getModelFromChange"
9app/models/models.js:77 "process_delta"
10app/models/models.js:333 "add"
11app/models/models.js:347 "trim"
12app/models/models.js:116 "process_delta"
13app/models/models.js:313 "setter"
14app/models/models.js:157 "get_informative_states_for_service"
15app/models/models.js:138 "get_units_for_service"
16app/models/models.js:120 "_setDefaultsAndCalculatedValues"
17app/models/models.js:454 "reset"
18app/models/models.js:393 "initializer"
19app/models/models.js:305 "valueFn"
20app/models/models.js:239 "process_delta"
21app/models/models.js:128 "add"
22app/models/models.js:212 "process_delta"
23app/models/models.js:353 "removeOldest"
24app/models/models.js:438 "getModelListByModelName"
25app/models/models.js:281 "get_relations_for_service"
26app/models/models.js:366 "getNotificationsForModel"
27app/models/models.js:248 "has_relation_for_endpoint"
28app/models/models.js:338 "comparator"
29app/models/models.js:463 "on_delta"
30app/models/models.js:429 "getModelById"
31app/store/charm.js:23 "success"1app/store/charm.js:23 "success"
32app/store/charm.js:125 "setter"2app/store/charm.js:125 "setter"
33app/store/charm.js:19 "loadByPath"3app/store/charm.js:19 "loadByPath"
@@ -39,18 +9,18 @@
39app/store/notifications.js:107 "level"9app/store/notifications.js:107 "level"
40app/store/notifications.js:136 "title"10app/store/notifications.js:136 "title"
41app/store/notifications.js:32 "message"11app/store/notifications.js:32 "message"
42app/store/env/python.js:261 "status"12app/store/env/python.js:87 "_dispatch_rpc_result"
43app/store/env/python.js:71 "_dispatch_event"13app/store/env/python.js:364 "get_endpoints"
44app/store/env/python.js:79 "_dispatch_rpc_result"14app/store/env/python.js:184 "get_charm"
45app/store/env/python.js:356 "get_endpoints"15app/store/env/python.js:269 "status"
46app/store/env/python.js:34 "initializer"16app/store/env/python.js:34 "initializer"
47app/store/env/python.js:176 "get_charm"17app/store/env/python.js:79 "_dispatch_event"
48app/store/env/python.js:180 "get_service"18app/store/env/python.js:188 "get_service"
19app/store/env/base.js:53 "destructor"
49app/store/env/base.js:38 "initializer"20app/store/env/base.js:38 "initializer"
50app/store/env/base.js:53 "destructor"21app/store/env/base.js:75 "on_close"
51app/store/env/base.js:58 "connect"22app/store/env/base.js:58 "connect"
52app/store/env/base.js:73 "on_open"23app/store/env/base.js:73 "on_open"
53app/store/env/base.js:75 "on_close"
54app/views/utils.js:287 "removeSVGClass"24app/views/utils.js:287 "removeSVGClass"
55app/views/utils.js:266 "addSVGClass"25app/views/utils.js:266 "addSVGClass"
56app/views/utils.js:807 "value"26app/views/utils.js:807 "value"
@@ -116,47 +86,37 @@
116app/views/unit.js:157 "confirmRemoved"86app/views/unit.js:157 "confirmRemoved"
117app/views/unit.js:185 "_removeUnitCallback"87app/views/unit.js:185 "_removeUnitCallback"
118app/views/service.js:191 "destroyService"88app/views/service.js:191 "destroyService"
89app/views/service.js:486 "_removeRelationCallback"
90app/views/service.js:795 "_setConfigCallback"
119app/views/service.js:269 "exposeService"91app/views/service.js:269 "exposeService"
120app/views/service.js:485 "_removeRelationCallback"92app/views/service.js:915 "filterUnits"
121app/views/service.js:276 "_exposeServiceCallback"93app/views/service.js:276 "_exposeServiceCallback"
122app/views/service.js:175 "confirmDestroy"94app/views/service.js:175 "confirmDestroy"
123app/views/service.js:200 "_destroyCallback"95app/views/service.js:200 "_destroyCallback"
124app/views/service.js:353 "fitToWindow"96app/views/service.js:550 "_setConstraintsCallback"
125app/views/service.js:549 "_setConstraintsCallback"
126app/views/service.js:712 "render"
127app/views/service.js:33 "resetUnits"97app/views/service.js:33 "resetUnits"
128app/views/service.js:436 "confirmRemoved"98app/views/service.js:765 "saveConfig"
129app/views/service.js:724 "showErrors"99app/views/service.js:725 "showErrors"
130app/views/service.js:424 "render"100app/views/service.js:713 "render"
131app/views/service.js:354 "getHeight"101app/views/service.js:637 "render"
132app/views/service.js:62 "_modifyUnits"102app/views/service.js:62 "_modifyUnits"
133app/views/service.js:40 "modifyUnits"103app/views/service.js:40 "modifyUnits"
104app/views/service.js:903 "render"
105app/views/service.js:354 "fitToWindow"
134app/views/service.js:126 "_removeUnitCallback"106app/views/service.js:126 "_removeUnitCallback"
135app/views/service.js:458 "doRemoveRelation"
136app/views/service.js:242 "unexposeService"107app/views/service.js:242 "unexposeService"
137app/views/service.js:523 "updateConstraints"108app/views/service.js:425 "render"
138app/views/service.js:902 "render"109app/views/service.js:355 "getHeight"
139app/views/service.js:914 "filterUnits"110app/views/service.js:459 "doRemoveRelation"
140app/views/service.js:98 "_addUnitCallback"111app/views/service.js:98 "_addUnitCallback"
141app/views/service.js:764 "saveConfig"112app/views/service.js:524 "updateConstraints"
142app/views/service.js:636 "render"113app/views/service.js:437 "confirmRemoved"
143app/views/service.js:315 "getServiceTabs"114app/views/service.js:315 "getServiceTabs"
144app/views/service.js:303 "initializer"115app/views/service.js:303 "initializer"
145app/views/service.js:249 "_unexposeServiceCallback"116app/views/service.js:249 "_unexposeServiceCallback"
146app/views/service.js:794 "_setConfigCallback"
147app/views/topology/panzoom.js:52 "renderSlider"117app/views/topology/panzoom.js:52 "renderSlider"
148app/views/topology/panzoom.js:193 "renderedHandler"118app/views/topology/panzoom.js:193 "renderedHandler"
149app/views/topology/panzoom.js:38 "componentBound"119app/views/topology/panzoom.js:38 "componentBound"
150app/views/topology/topology.js:180 "getter"
151app/views/topology/topology.js:72 "renderOnce"
152app/views/topology/topology.js:148 "serviceForBox"
153app/views/topology/topology.js:168 "getter"
154app/views/topology/topology.js:118 "computeScales"
155app/views/topology/topology.js:181 "setter"
156app/views/topology/topology.js:172 "getter"
157app/views/topology/topology.js:32 "initializer"
158app/views/topology/topology.js:185 "getter"
159app/views/topology/topology.js:186 "setter"
160app/views/topology/relation.js:73 "render"120app/views/topology/relation.js:73 "render"
161app/views/topology/relation.js:188 "drawRelationGroup"121app/views/topology/relation.js:188 "drawRelationGroup"
162app/views/topology/relation.js:440 "addRelationDragEnd"122app/views/topology/relation.js:440 "addRelationDragEnd"
@@ -205,3 +165,43 @@
205app/views/topology/service.js:946 "destroyServiceConfirm"165app/views/topology/service.js:946 "destroyServiceConfirm"
206app/views/topology/service.js:249 "serviceAddRelMouseDown"166app/views/topology/service.js:249 "serviceAddRelMouseDown"
207app/views/topology/service.js:128 "serviceMouseEnter"167app/views/topology/service.js:128 "serviceMouseEnter"
168app/views/topology/topology.js:180 "getter"
169app/views/topology/topology.js:72 "renderOnce"
170app/views/topology/topology.js:148 "serviceForBox"
171app/views/topology/topology.js:168 "getter"
172app/views/topology/topology.js:118 "computeScales"
173app/views/topology/topology.js:181 "setter"
174app/views/topology/topology.js:172 "getter"
175app/views/topology/topology.js:32 "initializer"
176app/views/topology/topology.js:185 "getter"
177app/views/topology/topology.js:186 "setter"
178app/models/models.js:179 "update_service_unit_aggregates"
179app/models/models.js:445 "getModelFromChange"
180app/models/models.js:77 "process_delta"
181app/models/models.js:333 "add"
182app/models/models.js:347 "trim"
183app/models/models.js:116 "process_delta"
184app/models/models.js:313 "setter"
185app/models/models.js:157 "get_informative_states_for_service"
186app/models/models.js:138 "get_units_for_service"
187app/models/models.js:120 "_setDefaultsAndCalculatedValues"
188app/models/models.js:454 "reset"
189app/models/models.js:393 "initializer"
190app/models/models.js:305 "valueFn"
191app/models/models.js:239 "process_delta"
192app/models/models.js:128 "add"
193app/models/models.js:212 "process_delta"
194app/models/models.js:353 "removeOldest"
195app/models/models.js:438 "getModelListByModelName"
196app/models/models.js:281 "get_relations_for_service"
197app/models/models.js:366 "getNotificationsForModel"
198app/models/models.js:248 "has_relation_for_endpoint"
199app/models/models.js:338 "comparator"
200app/models/models.js:463 "on_delta"
201app/models/models.js:429 "getModelById"
202app/models/charm.js:117 "parse"
203app/models/charm.js:134 "compare"
204app/models/charm.js:108 "failure"
205app/models/charm.js:80 "sync"
206app/models/charm.js:50 "initializer"
207app/models/charm.js:160 "validator"

Subscribers

People subscribed via source and target branches