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
1=== modified file 'app/app.js'
2--- app/app.js 2013-02-15 11:58:05 +0000
3+++ app/app.js 2013-02-20 04:24:21 +0000
4@@ -232,6 +232,13 @@
5 consoleManager.noop();
6 }
7 }
8+
9+ // Namespaced URL tracker.
10+ this._routeGeneration = 0;
11+ this._routeStates = {};
12+ this._nsRouter = juju.Router('charmstore');
13+
14+
15 // Create a client side database to store state.
16 this.db = new models.Database();
17 this.serviceEndpoints = {};
18@@ -336,6 +343,136 @@
19 },
20
21 /**
22+ * NS aware navigate wrapper. This has the feature
23+ * of preserving existing namespaces in the url.
24+ *
25+ * @method _navigate
26+ **/
27+ _navigate: function(url, options) {
28+ var loc = Y.getLocation();
29+ var qs = this._nsRouter.getQS(url);
30+ var result = this._nsRouter.combine(loc.pathname, url);
31+ if (qs) {
32+ result += '?' + qs;
33+ }
34+ if (JujuGUI.superclass._navigate.call(this, url, options)) {
35+ // Queue/Save the entire URL, not just the new fragment.
36+ this._queue(result, true);
37+ return true;
38+ }
39+ return false;
40+ },
41+
42+
43+ /**
44+ * Null-queue for NS routing. The 1ms delay in the queue presents problems,
45+ * apply url saves as they happen.
46+ *
47+ * Overrides superclass, formalizes dependency on HTML5 paths.
48+ * @method _queue
49+ **/
50+ _queue: function() {
51+ // Sync Invocation
52+ this._save.apply(this, arguments);
53+ },
54+
55+ /**
56+ Dispatches to the first route handler that matches the specified _path_.
57+
58+ If called before the `ready` event has fired, the dispatch will be aborted.
59+ This ensures normalized behavior between Chrome (which fires a `popstate`
60+ event on every pageview) and other browsers (which do not).
61+
62+ @method _dispatch
63+ @param {String} path URL path.
64+ @param {String} url Full URL.
65+ @param {String} src What initiated the dispatch.
66+ @chainable
67+ @protected
68+ **/
69+ _dispatch: function(path, url, src) {
70+ var self = this,
71+ routes,
72+ callbacks = [],
73+ namespaces = [],
74+ matches, req, res, parts;
75+
76+ self._dispatching = self._dispatched = true;
77+
78+ parts = this._nsRouter.split(path);
79+ namespaces = this._nsRouter.parse(parts.pathname);
80+ this._routeGeneration += 1;
81+
82+ Y.each(namespaces, function(fragment, namespace) {
83+ routes = self.match(fragment);
84+ if (!routes || !routes.length) {
85+ self._dispatching = false;
86+ return self;
87+ }
88+
89+ //console.log("_dispatch", fragment, url, src);
90+ req = self._getRequest(fragment, url, src);
91+ res = self._getResponse(req);
92+
93+ req.next = function(err) {
94+ var callback, route;
95+
96+ if (err) {
97+ // Special case "route" to skip to the next route handler
98+ // avoiding any additional callbacks for the current route.
99+ if (err === 'route') {
100+ callbacks = [];
101+ req.next();
102+ } else {
103+ Y.error(err);
104+ }
105+
106+ } else if ((callback = callbacks.shift())) {
107+ //console.group('callback');
108+ //console.log('callback', callback);
109+ if (typeof callback === 'string') {
110+ callback = self[callback];
111+ }
112+
113+ // Allow access to the num or remaining callbacks for the route.
114+ req.pendingCallbacks = callbacks.length;
115+ // Attach the callback id to the request.
116+ req.callbackId = Y.stamp(callback, true);
117+ callback.call(self, req, res, req.next);
118+ //console.groupEnd();
119+
120+ } else if ((route = routes.shift())) {
121+ // Make a copy of this route's `callbacks` and find its matches.
122+ //console.group("route", fragment, route.path)
123+ callbacks = route.callbacks.concat();
124+ matches = route.regex.exec(fragment);
125+
126+ // Use named keys for parameter names if the route path contains
127+ // named keys. Otherwise, use numerical match indices.
128+ if (matches.length === route.keys.length + 1) {
129+ req.params = Y.Array.hash(route.keys, matches.slice(1));
130+ } else {
131+ req.params = matches.concat();
132+ }
133+
134+ // Allow access tot he num of remaining routes for this request.
135+ req.pendingRoutes = routes.length;
136+
137+ // Execute this route's `callbacks`.
138+ req.next();
139+ //console.groupEnd();
140+ }
141+ };
142+
143+ req.next();
144+ });
145+
146+
147+ self._dispatching = false;
148+ return self._dequeue();
149+ },
150+
151+ /**
152 * Hook up all of the declared behaviors.
153 *
154 * @method enableBehaviors
155@@ -466,9 +603,6 @@
156 * @private
157 */
158 _buildServiceView: function(req, viewName) {
159- console.log('App: Route: Service',
160- viewName, req.params.id, req.path, req.pendingRoutes);
161-
162 var service = this.db.services.getById(req.params.id);
163 this._prefetch_service(service);
164 this.showView(viewName, {
165@@ -518,7 +652,6 @@
166 * @method show_charm_collection
167 */
168 show_charm_collection: function(req) {
169- console.log('App: Route: Charm Collection', req.path, req.query);
170 this.showView('charm_collection', {
171 query: req.query.q,
172 charm_store: this.charm_store
173@@ -529,7 +662,6 @@
174 * @method show_charm
175 */
176 show_charm: function(req) {
177- console.log('App: Route: Charm', req.path, req.params);
178 var charm_url = req.params.charm_store_path;
179 this.showView('charm', {
180 charm_data_url: charm_url,
181@@ -644,9 +776,14 @@
182 * @private
183 */
184 onLogin: function() {
185- Y.one('body > #full-screen-mask').hide();
186- // Stop the animated loading spinner.
187- spinner.stop();
188+ var mask = Y.one('#full-screen-mask');
189+ if (mask) {
190+ mask.hide();
191+ // Stop the animated loading spinner.
192+ if (spinner) {
193+ spinner.stop();
194+ }
195+ }
196 this.dispatch();
197 },
198
199@@ -776,12 +913,13 @@
200
201 // Replace the path params with items from the model's attributes.
202 path = path.replace(regexPathParam,
203- function(match, operator, key) {
204- if (reverse_map !== undefined && reverse_map[key]) {
205- key = reverse_map[key];
206- }
207- return attrs[key];
208- });
209+ function(match, operator, key) {
210+ if (reverse_map !== undefined &&
211+ reverse_map[key]) {
212+ key = reverse_map[key];
213+ }
214+ return attrs[key];
215+ });
216 matches.push(Y.mix({path: path,
217 route: route,
218 attrs: attrs,
219@@ -814,12 +952,51 @@
220 Y.Array.each(routes, function(route) {
221 // Additionally pass route as options. This is needed to pass through
222 // the attribute setter.
223- this.route(route.path, route.callback, route);
224+ // Callback can be an array, we push a state tracker to the head of
225+ // each callback chain.
226+ var callbacks = route.callbacks || route.callback;
227+ if (!Y.Lang.isArray(callbacks)) {
228+ callbacks = [callbacks];
229+ }
230+ // Tag each callback such that we can resolve it in
231+ // the state tracker.
232+ Y.Array.each(callbacks, function(cb) {Y.stamp(cb);});
233+ // Inject our state tracker
234+ if (callbacks[0] !== '_routeStateTracker') {
235+ callbacks.unshift('_routeStateTracker');
236+ }
237+ route.callbacks = callbacks.concat();
238+ // Additionally pass the route with its exteneded
239+ // attribute set.
240+ this.route(route.path, route.callbacks, route);
241 }, this);
242 return this._routes.concat();
243 },
244
245 /**
246+ * Internal state tracker, makes sure a given route
247+ * dispatches once per any dispatch call with regard to
248+ * namespace components.
249+ *
250+ * @method _routeStateTracker
251+ **/
252+ _routeStateTracker: function(req, res, next) {
253+ var gen = this._routeGeneration,
254+ seen = this._routeStates,
255+ callbackId = req.callbackId;
256+
257+ if (callbackId && seen[callbackId] && (seen[callbackId] >= gen)) {
258+ // Calling next with a route error aborts
259+ // further callbacks in _this_ array (remember
260+ // route.callbacks is an array per route).
261+ // But can allow continued processing;
262+ next('route');
263+ }
264+ next();
265+ seen[callbackId] = gen;
266+ },
267+
268+ /**
269 * Wrap the default routing using a whitelist to avoid extra juggling.
270 *
271 * @method route
272@@ -843,6 +1020,7 @@
273
274 }, {
275 ATTRS: {
276+ html5: true,
277 charm_store: {},
278
279 /*
280@@ -859,39 +1037,39 @@
281 routes: {
282 value: [
283 // Called on each request.
284- { path: '*', callback: 'check_user_credentials'},
285- { path: '*', callback: 'show_notifications_view'},
286+ { path: '*', callbacks: 'check_user_credentials'},
287+ { path: '*', callbacks: 'show_notifications_view'},
288 // Charms.
289- { path: '/charms/', callback: 'show_charm_collection'},
290- { path: '/charms/*charm_store_path',
291- callback: 'show_charm',
292+ { path: '/charms/', callbacks: 'show_charm_collection'},
293+ { path: '/charms/*charm_store_path/',
294+ callbacks: 'show_charm',
295 model: 'charm'},
296 // Notifications.
297 { path: '/notifications/',
298- callback: 'show_notifications_overview'},
299+ callbacks: 'show_notifications_overview'},
300 // Services.
301- { path: '/service/:id/config',
302- callback: 'show_service_config',
303+ { path: '/service/:id/config/',
304+ callbacks: 'show_service_config',
305 intent: 'config',
306 model: 'service'},
307- { path: '/service/:id/constraints',
308- callback: 'show_service_constraints',
309+ { path: '/service/:id/constraints/',
310+ callbacks: 'show_service_constraints',
311 intent: 'constraints',
312 model: 'service'},
313- { path: '/service/:id/relations',
314- callback: 'show_service_relations',
315+ { path: '/service/:id/relations/',
316+ callbacks: 'show_service_relations',
317 intent: 'relations',
318 model: 'service'},
319 { path: '/service/:id/',
320- callback: 'show_service',
321+ callbacks: 'show_service',
322 model: 'service'},
323 // Units.
324 { path: '/unit/:id/',
325- callback: 'show_unit',
326+ callbacks: 'show_unit',
327 reverse_map: {id: 'urlName'},
328 model: 'serviceUnit'},
329 // Root.
330- { path: '/', callback: 'show_environment'}
331+ { path: '/', callbacks: 'show_environment'}
332 ]
333 }
334 }
335@@ -906,6 +1084,7 @@
336 'juju-charm-store',
337 'juju-models',
338 'juju-notifications',
339+ 'juju-routing',
340 // This alias does not seem to work, including references by hand.
341 'juju-controllers',
342 'juju-notification-controller',
343
344=== added file 'app/assets/javascripts/routing.js'
345--- app/assets/javascripts/routing.js 1970-01-01 00:00:00 +0000
346+++ app/assets/javascripts/routing.js 2013-02-20 04:24:21 +0000
347@@ -0,0 +1,217 @@
348+'use strict';
349+
350+YUI.add('juju-routing', function(Y) {
351+
352+ function _trim(s, char, leading, trailing) {
353+ // remove leading, trailing char.
354+ while (leading && s && s.indexOf(char) === 0) {
355+ s = s.slice(1, s.length);
356+ }
357+ while (trailing && s && s.lastIndexOf(char) === (s.length - 1)) {
358+ s = s.slice(0, s.length - 1);
359+ }
360+ return s;
361+ }
362+
363+ function trim(s, char) { return _trim(s, char, true, true); }
364+ function rtrim(s, char) { return _trim(s, char, false, true); }
365+ function ltrim(s, char) { return _trim(s, char, true, false); }
366+
367+
368+ /**
369+ * Return a sorted array of namespace, url pairs.
370+ *
371+ * @method pairs
372+ * @return {Object} [[namespace, url]].
373+ **/
374+ function pairs(o) {
375+ var result = [],
376+ keys = Y.Object.keys(o).sort();
377+
378+ Y.each(keys, function(k) {
379+ result.push([k, o[k]]);
380+ });
381+ return result;
382+ }
383+
384+ var Routes = {
385+ pairs: function() {return pairs(this);}
386+ };
387+
388+ // Multi dimensional router (TARDIS).
389+ var _Router = {
390+ // Regex to parse namespace, url fragment pairs.
391+ _fragment: /\/?(:\w+\:)/,
392+ _regexUrlOrigin: /^(?:[^\/#?:]+:\/\/|\/\/)[^\/]*/,
393+ defaultNamespace: 'default',
394+
395+ /**
396+ * Return the query string portion of a URL.
397+ * @method getQS
398+ **/
399+ getQS: function(url) {
400+ return url.split('?')[1];
401+ },
402+
403+ /**
404+ * Split a URL into components, a subset of the
405+ * Location Object.
406+ *
407+ * @method split
408+ * @param {String} url to split.
409+ * @return {Object} hash of URL parts.
410+ **/
411+ split: function(url) {
412+ var result = {
413+ href: url
414+ };
415+ var origin = this._regexUrlOrigin.exec(url);
416+ result.search = this.getQS(url);
417+
418+ if (origin) {
419+ // Take the match.
420+ result.origin = origin = origin[0];
421+ // And remove it from the url.
422+ result.pathname = url.substr(origin.length);
423+ } else {
424+ result.pathname = url;
425+ }
426+
427+ if (result.search) {
428+ result.pathname = result.pathname.substr(0,
429+ (result.pathname.length - result.search.length) - 1);
430+ }
431+
432+ return result;
433+ },
434+
435+ /**
436+ * Parse a url into an Object with namespaced url fragments for values.
437+ * Each value will be normalized to include a trailing slash
438+ * ('/').
439+ *
440+ * @method parse
441+ * @param {String} url to parse.
442+ * @return {Object} result is {ns: url fragment {String}}.
443+ **/
444+ parse: function(url) {
445+ var result = Object.create(Routes, {
446+ defaultNamespacePresent: {
447+ enumerable: false,
448+ writable: true
449+ }
450+ });
451+ var parts = this.split(url);
452+ url = parts.pathname;
453+
454+ parts = url.split(this._fragment);
455+ // > '/foo/bar'.split(this._fragment)
456+ // ["/foo/bar"]
457+ // > :baz:/foo/bar'.split(this._fragment)
458+ // ["", ":baz:", "/foo/bar"]
459+ if (parts[0]) {
460+ // This is a URL fragment without a namespace.
461+ parts[0] = rtrim(parts[0], '/') + '/';
462+ result[this.defaultNamespace] = parts[0];
463+ result.defaultNamespacePresent = true;
464+ } else {
465+ result[this.defaultNamespace] = '/'; // A sane default.
466+ }
467+ // Now scan each pair after [0] for ns/route elements
468+ for (var i = 1; i < parts.length; i += 2) {
469+ var ns = trim(parts[i], ':'),
470+ val = '/';
471+
472+ if ((i + 1) > parts.length) {
473+ console.log('URL namespace without path');
474+ } else {
475+ val = parts[i + 1];
476+ }
477+
478+ if (result[ns] !== undefined) {
479+ console.log('URL has more than one reference to same namespace');
480+ }
481+ if (ns === this.defaultNamespace) {
482+ result.defaultNamespacePresent = true;
483+ }
484+ result[ns] = rtrim(val, '/') + '/';
485+ }
486+ return result;
487+ },
488+
489+ /**
490+ * Given an object with ns:url_fragment pairs
491+ * produce a properly ordered url.
492+ *
493+ * @method url
494+ * @param {Object} componets is the result of a parse(url) call.
495+ * @return {String} url.
496+ **/
497+ url: function(components) {
498+ var base = Y.mix({}, components);
499+ var url = '/';
500+
501+ function slash(u) {
502+ if (u.lastIndexOf('/') !== u.length - 1) {
503+ u += '/';
504+ }
505+ return u;
506+ }
507+
508+ if (base[this.defaultNamespace]) {
509+ url += trim(base[this.defaultNamespace], '/');
510+ delete base[this.defaultNamespace];
511+ }
512+
513+ // Sort base properties such
514+ // that output ordering is uniform.
515+ var keys = Y.Object.keys(base).sort();
516+ Y.each(keys, function(ns) {
517+ url = slash(url);
518+ url += ':' + ns + ':' + base[ns];
519+ });
520+
521+ url = slash(url);
522+ return url;
523+ },
524+
525+ /**
526+ * Smartly combine new namespaced url components with old.
527+ *
528+ * @method combine
529+ * @param {Object} orig url.
530+ * @param {Object} incoming new url.
531+ * @return {String} a new namespaced url.
532+ **/
533+ combine: function(orig, incoming) {
534+ var url;
535+
536+ if (Y.Lang.isString(orig)) {
537+ orig = this.parse(orig);
538+ }
539+ if (Y.Lang.isString(incoming)) {
540+ incoming = this.parse(incoming);
541+ }
542+
543+ if (!incoming.defaultNamespacePresent) {
544+ // The default namespace was supplied (rather
545+ // than defaulting to /) in the incoming url,
546+ // this means we can safely override it.
547+ delete incoming[this.defaultNamespace];
548+ }
549+ url = this.url(Y.mix(orig, incoming, true, Y.Object.keys(incoming)));
550+ return url;
551+
552+ }
553+ };
554+
555+ Y.namespace('juju').Router = function(defaultNamespace) {
556+ var r = Object.create(_Router);
557+ r.defaultNamespace = defaultNamespace;
558+ return r;
559+ };
560+
561+
562+}, '0.1.0', {
563+ requires: ['oop']
564+});
565
566=== modified file 'app/modules-debug.js'
567--- app/modules-debug.js 2013-02-15 11:58:05 +0000
568+++ app/modules-debug.js 2013-02-20 04:24:21 +0000
569@@ -52,6 +52,10 @@
570 fullpath: '/juju-ui/assets/javascripts/reconnecting-websocket.js'
571 },
572
573+ 'juju-routing': {
574+ fullpath: '/juju-ui/assets/javascripts/routing.js'
575+ },
576+
577 // Views
578 'juju-topology-relation': {
579 fullpath: '/juju-ui/views/topology/relation.js'
580
581=== modified file 'app/views/service.js'
582--- app/views/service.js 2013-02-08 17:36:58 +0000
583+++ app/views/service.js 2013-02-20 04:24:21 +0000
584@@ -315,20 +315,21 @@
585 getServiceTabs: function(href) {
586 var db = this.get('db'),
587 service = this.get('model'),
588+ getModelURL = this.get('getModelURL'),
589 charmId = service.get('charm'),
590 charm = db.charms.getById(charmId),
591- charmUrl = (charm ? this.get('getModelURL')(charm) : '#');
592+ charmUrl = (charm ? getModelURL(charm) : '#');
593
594 var tabs = [{
595- href: '.',
596+ href: getModelURL(service),
597 title: 'Units',
598 active: false
599 }, {
600- href: 'relations',
601+ href: getModelURL(service, 'relations'),
602 title: 'Relations',
603 active: false
604 }, {
605- href: 'config',
606+ href: getModelURL(service, 'config'),
607 title: 'Settings',
608 active: false
609 }, {
610@@ -336,7 +337,7 @@
611 title: 'Charm',
612 active: false
613 }, {
614- href: 'constraints',
615+ href: getModelURL(service, 'constraints'),
616 title: 'Constraints',
617 active: false
618 }];
619
620=== modified file 'bin/merge-files'
621--- bin/merge-files 2013-02-13 18:28:30 +0000
622+++ bin/merge-files 2013-02-20 04:24:21 +0000
623@@ -72,6 +72,7 @@
624 'app/assets/javascripts/d3.v2.min.js',
625 'app/assets/javascripts/d3-components.js',
626 'app/assets/javascripts/reconnecting-websocket.js',
627+ 'app/assets/javascripts/routing.js',
628 'app/assets/javascripts/gallery-ellipsis.js',
629 'app/assets/javascripts/gallery-markdown.js',
630 'app/assets/javascripts/gallery-timer.js']);
631
632=== modified file 'test/index.html'
633--- test/index.html 2013-02-15 12:29:14 +0000
634+++ test/index.html 2013-02-20 04:24:21 +0000
635@@ -29,6 +29,7 @@
636
637
638 <!-- Tests (Alphabetical)-->
639+
640 <script src="test_app.js"></script>
641 <script src="test_app_hotkeys.js"></script>
642 <script src="test_application_notifications.js"></script>
643@@ -48,6 +49,7 @@
644 <script src="test_notifications.js"></script>
645 <script src="test_notifier_widget.js"></script>
646 <script src="test_panzoom.js"></script>
647+ <script src="test_routing.js"></script>
648 <script src="test_service_config_view.js"></script>
649 <script src="test_service_module.js"></script>
650 <script src="test_service_view.js"></script>
651@@ -58,7 +60,6 @@
652 <script src="test_unit_view.js"></script>
653 <script src="test_utils.js"></script>
654 <script src="test_viewport_module.js"></script>
655-
656 <script>
657 YUI_config = {
658 async: false,
659
660=== modified file 'test/test_app.js'
661--- test/test_app.js 2013-02-15 11:58:05 +0000
662+++ test/test_app.js 2013-02-20 04:24:21 +0000
663@@ -113,14 +113,14 @@
664 app.getModelURL(wordpress).should.equal('/service/wordpress/');
665 // However, passing 'intent' can force selection of another one.
666 app.getModelURL(wordpress, 'config').should.equal(
667- '/service/wordpress/config');
668+ '/service/wordpress/config/');
669
670 // Service units use argument rewriting (thus not /u/wp/0).
671 app.getModelURL(wp0).should.equal('/unit/wordpress-0/');
672
673 // Charms also require a mapping, but only a name, not a function.
674 app.getModelURL(wp_charm).should.equal(
675- '/charms/charms/precise/wordpress-6/json');
676+ '/charms/charms/precise/wordpress-6/json/');
677 });
678
679 it('should display the configured environment name', function() {
680
681=== added file 'test/test_routing.js'
682--- test/test_routing.js 1970-01-01 00:00:00 +0000
683+++ test/test_routing.js 2013-02-20 04:24:21 +0000
684@@ -0,0 +1,108 @@
685+
686+'use strict';
687+
688+describe('Namespaced Routing', function() {
689+ var Y, juju, app;
690+
691+ before(function(done) {
692+ Y = YUI(GlobalConfig).use(['juju-routing',
693+ 'juju-gui'],
694+ function(Y) {
695+ juju = Y.namespace('juju');
696+ done();
697+ });
698+ });
699+
700+ beforeEach(function() {
701+ app = new juju.App();
702+ });
703+
704+ it('should support basic namespaced urls', function() {
705+ var router = juju.Router('charmstore');
706+
707+ var match = router.parse('/');
708+ assert(match.charmstore === '/');
709+ assert(match.inspector === undefined);
710+
711+ match = router.parse('cs:mysql');
712+ assert(match.charmstore === 'cs:mysql/');
713+ assert(match.inspector === undefined);
714+
715+ match = router.parse(
716+ '/:inspector:/services/mysql/:charmstore:/charms/precise/mediawiki');
717+ assert(match.charmstore === '/charms/precise/mediawiki/');
718+ assert(match.inspector === '/services/mysql/');
719+
720+ match.pairs().should.eql(
721+ [['charmstore', '/charms/precise/mediawiki/'],
722+ ['inspector', '/services/mysql/']]);
723+
724+ match = router.parse(
725+ '/charms/precise/mediawiki/:inspector:/services/mysql/');
726+ assert(match.charmstore === '/charms/precise/mediawiki/');
727+ assert(match.inspector === '/services/mysql/');
728+
729+ var u = router.url(match);
730+ assert(u === '/charms/precise/mediawiki/:inspector:/services/mysql/');
731+
732+ // Sorted keys.
733+ u = router.url({charmstore: '/', gamma: 'g', a: 'alpha', b: 'beta'});
734+ assert(u === '/:a:alpha/:b:beta/:gamma:g/');
735+
736+ // Sorted keys with actual charmstore [defaultNamespace] component.
737+ u = router.url({
738+ charmstore: '/charms/precise/mysql',
739+ gamma: 'g', a: 'alpha', b: 'beta'});
740+ assert(u === '/charms/precise/mysql/:a:alpha/:b:beta/:gamma:g/');
741+
742+ u = router.url({gamma: 'g', a: 'alpha', b: 'beta'});
743+ assert(u === '/:a:alpha/:b:beta/:gamma:g/');
744+ });
745+
746+
747+ it('should support a default namespace', function() {
748+ var router = juju.Router('charmstore');
749+ var url, parts;
750+ router.defaultNamespace.should.equal('charmstore');
751+
752+ // Use a different namespace.
753+ router = juju.Router('foo');
754+ parts = router.parse('/bar');
755+ url = router.url(parts);
756+ url.should.equal('/bar/');
757+
758+ // .. and with an additional ns.
759+ parts.extra = 'happens';
760+ url = router.url(parts);
761+ url.should.equal('/bar/:extra:happens/');
762+ });
763+
764+
765+ it('should be able to cleanly combine urls preserving untouched namespaces',
766+ function() {
767+ var router = juju.Router('charmstore');
768+ var url, parts;
769+ url = router.combine('/foo/bar', '/:inspector:/foo/');
770+ url.should.equal('/foo/bar/:inspector:/foo/');
771+ });
772+
773+
774+ it('should be able to split qualified urls', function() {
775+ var router = juju.Router('playground');
776+ var url, parts;
777+
778+ parts = router.split('http://foo.bar:8888/foo/bar');
779+ parts.pathname.should.equal('/foo/bar');
780+ parts.origin.should.equal('http://foo.bar:8888');
781+
782+ parts = router.split('http://foo.bar:8888/foo/bar/?a=b');
783+ parts.pathname.should.equal('/foo/bar/');
784+ parts.origin.should.equal('http://foo.bar:8888');
785+ parts.search.should.equal('a=b');
786+
787+ parts = router.split('/foo/bar/?a=b');
788+ parts.pathname.should.equal('/foo/bar/');
789+ parts.search.should.equal('a=b');
790+ });
791+
792+});
793
794=== modified file 'undocumented'
795--- undocumented 2013-02-18 09:42:16 +0000
796+++ undocumented 2013-02-20 04:24:21 +0000
797@@ -1,33 +1,3 @@
798-app/models/charm.js:117 "parse"
799-app/models/charm.js:134 "compare"
800-app/models/charm.js:108 "failure"
801-app/models/charm.js:80 "sync"
802-app/models/charm.js:50 "initializer"
803-app/models/charm.js:160 "validator"
804-app/models/models.js:179 "update_service_unit_aggregates"
805-app/models/models.js:445 "getModelFromChange"
806-app/models/models.js:77 "process_delta"
807-app/models/models.js:333 "add"
808-app/models/models.js:347 "trim"
809-app/models/models.js:116 "process_delta"
810-app/models/models.js:313 "setter"
811-app/models/models.js:157 "get_informative_states_for_service"
812-app/models/models.js:138 "get_units_for_service"
813-app/models/models.js:120 "_setDefaultsAndCalculatedValues"
814-app/models/models.js:454 "reset"
815-app/models/models.js:393 "initializer"
816-app/models/models.js:305 "valueFn"
817-app/models/models.js:239 "process_delta"
818-app/models/models.js:128 "add"
819-app/models/models.js:212 "process_delta"
820-app/models/models.js:353 "removeOldest"
821-app/models/models.js:438 "getModelListByModelName"
822-app/models/models.js:281 "get_relations_for_service"
823-app/models/models.js:366 "getNotificationsForModel"
824-app/models/models.js:248 "has_relation_for_endpoint"
825-app/models/models.js:338 "comparator"
826-app/models/models.js:463 "on_delta"
827-app/models/models.js:429 "getModelById"
828 app/store/charm.js:23 "success"
829 app/store/charm.js:125 "setter"
830 app/store/charm.js:19 "loadByPath"
831@@ -39,18 +9,18 @@
832 app/store/notifications.js:107 "level"
833 app/store/notifications.js:136 "title"
834 app/store/notifications.js:32 "message"
835-app/store/env/python.js:261 "status"
836-app/store/env/python.js:71 "_dispatch_event"
837-app/store/env/python.js:79 "_dispatch_rpc_result"
838-app/store/env/python.js:356 "get_endpoints"
839+app/store/env/python.js:87 "_dispatch_rpc_result"
840+app/store/env/python.js:364 "get_endpoints"
841+app/store/env/python.js:184 "get_charm"
842+app/store/env/python.js:269 "status"
843 app/store/env/python.js:34 "initializer"
844-app/store/env/python.js:176 "get_charm"
845-app/store/env/python.js:180 "get_service"
846+app/store/env/python.js:79 "_dispatch_event"
847+app/store/env/python.js:188 "get_service"
848+app/store/env/base.js:53 "destructor"
849 app/store/env/base.js:38 "initializer"
850-app/store/env/base.js:53 "destructor"
851+app/store/env/base.js:75 "on_close"
852 app/store/env/base.js:58 "connect"
853 app/store/env/base.js:73 "on_open"
854-app/store/env/base.js:75 "on_close"
855 app/views/utils.js:287 "removeSVGClass"
856 app/views/utils.js:266 "addSVGClass"
857 app/views/utils.js:807 "value"
858@@ -116,47 +86,37 @@
859 app/views/unit.js:157 "confirmRemoved"
860 app/views/unit.js:185 "_removeUnitCallback"
861 app/views/service.js:191 "destroyService"
862+app/views/service.js:486 "_removeRelationCallback"
863+app/views/service.js:795 "_setConfigCallback"
864 app/views/service.js:269 "exposeService"
865-app/views/service.js:485 "_removeRelationCallback"
866+app/views/service.js:915 "filterUnits"
867 app/views/service.js:276 "_exposeServiceCallback"
868 app/views/service.js:175 "confirmDestroy"
869 app/views/service.js:200 "_destroyCallback"
870-app/views/service.js:353 "fitToWindow"
871-app/views/service.js:549 "_setConstraintsCallback"
872-app/views/service.js:712 "render"
873+app/views/service.js:550 "_setConstraintsCallback"
874 app/views/service.js:33 "resetUnits"
875-app/views/service.js:436 "confirmRemoved"
876-app/views/service.js:724 "showErrors"
877-app/views/service.js:424 "render"
878-app/views/service.js:354 "getHeight"
879+app/views/service.js:765 "saveConfig"
880+app/views/service.js:725 "showErrors"
881+app/views/service.js:713 "render"
882+app/views/service.js:637 "render"
883 app/views/service.js:62 "_modifyUnits"
884 app/views/service.js:40 "modifyUnits"
885+app/views/service.js:903 "render"
886+app/views/service.js:354 "fitToWindow"
887 app/views/service.js:126 "_removeUnitCallback"
888-app/views/service.js:458 "doRemoveRelation"
889 app/views/service.js:242 "unexposeService"
890-app/views/service.js:523 "updateConstraints"
891-app/views/service.js:902 "render"
892-app/views/service.js:914 "filterUnits"
893+app/views/service.js:425 "render"
894+app/views/service.js:355 "getHeight"
895+app/views/service.js:459 "doRemoveRelation"
896 app/views/service.js:98 "_addUnitCallback"
897-app/views/service.js:764 "saveConfig"
898-app/views/service.js:636 "render"
899+app/views/service.js:524 "updateConstraints"
900+app/views/service.js:437 "confirmRemoved"
901 app/views/service.js:315 "getServiceTabs"
902 app/views/service.js:303 "initializer"
903 app/views/service.js:249 "_unexposeServiceCallback"
904-app/views/service.js:794 "_setConfigCallback"
905 app/views/topology/panzoom.js:52 "renderSlider"
906 app/views/topology/panzoom.js:193 "renderedHandler"
907 app/views/topology/panzoom.js:38 "componentBound"
908-app/views/topology/topology.js:180 "getter"
909-app/views/topology/topology.js:72 "renderOnce"
910-app/views/topology/topology.js:148 "serviceForBox"
911-app/views/topology/topology.js:168 "getter"
912-app/views/topology/topology.js:118 "computeScales"
913-app/views/topology/topology.js:181 "setter"
914-app/views/topology/topology.js:172 "getter"
915-app/views/topology/topology.js:32 "initializer"
916-app/views/topology/topology.js:185 "getter"
917-app/views/topology/topology.js:186 "setter"
918 app/views/topology/relation.js:73 "render"
919 app/views/topology/relation.js:188 "drawRelationGroup"
920 app/views/topology/relation.js:440 "addRelationDragEnd"
921@@ -205,3 +165,43 @@
922 app/views/topology/service.js:946 "destroyServiceConfirm"
923 app/views/topology/service.js:249 "serviceAddRelMouseDown"
924 app/views/topology/service.js:128 "serviceMouseEnter"
925+app/views/topology/topology.js:180 "getter"
926+app/views/topology/topology.js:72 "renderOnce"
927+app/views/topology/topology.js:148 "serviceForBox"
928+app/views/topology/topology.js:168 "getter"
929+app/views/topology/topology.js:118 "computeScales"
930+app/views/topology/topology.js:181 "setter"
931+app/views/topology/topology.js:172 "getter"
932+app/views/topology/topology.js:32 "initializer"
933+app/views/topology/topology.js:185 "getter"
934+app/views/topology/topology.js:186 "setter"
935+app/models/models.js:179 "update_service_unit_aggregates"
936+app/models/models.js:445 "getModelFromChange"
937+app/models/models.js:77 "process_delta"
938+app/models/models.js:333 "add"
939+app/models/models.js:347 "trim"
940+app/models/models.js:116 "process_delta"
941+app/models/models.js:313 "setter"
942+app/models/models.js:157 "get_informative_states_for_service"
943+app/models/models.js:138 "get_units_for_service"
944+app/models/models.js:120 "_setDefaultsAndCalculatedValues"
945+app/models/models.js:454 "reset"
946+app/models/models.js:393 "initializer"
947+app/models/models.js:305 "valueFn"
948+app/models/models.js:239 "process_delta"
949+app/models/models.js:128 "add"
950+app/models/models.js:212 "process_delta"
951+app/models/models.js:353 "removeOldest"
952+app/models/models.js:438 "getModelListByModelName"
953+app/models/models.js:281 "get_relations_for_service"
954+app/models/models.js:366 "getNotificationsForModel"
955+app/models/models.js:248 "has_relation_for_endpoint"
956+app/models/models.js:338 "comparator"
957+app/models/models.js:463 "on_delta"
958+app/models/models.js:429 "getModelById"
959+app/models/charm.js:117 "parse"
960+app/models/charm.js:134 "compare"
961+app/models/charm.js:108 "failure"
962+app/models/charm.js:80 "sync"
963+app/models/charm.js:50 "initializer"
964+app/models/charm.js:160 "validator"

Subscribers

People subscribed via source and target branches