Merge lp:~bcsaller/juju-gui/namespace-routing into lp:juju-gui/experimental
- namespace-routing
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
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:/urlFragme
Each namespace in the url will be preserved and dispatched to accordingly.
Middleware should only fire once for a given request.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
- 373. By Benjamin Saller
-
merge trunk
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
File app/assets/
https:/
app/assets/
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:/
app/assets/
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:/
app/assets/
_trim(s, char, true, false); }
Wouldn't it be nice if the built-in trim were flexible enough for what
you need!
https:/
app/assets/
docstring would help with easy reading.
Return sorted array of object attribute pairs (name, value).
https:/
app/assets/
(TARDIS).
lol
https:/
app/assets/
trivial, but when placed next to your getQS implementation, "return
url.split('?')[0];" looks pretty reasonable.
https:/
app/assets/
url.split(
Might be helpful to include examples in comments:
> '/foo/bar'
["/foo/bar"]
'/> :baz:/foo/
["", ":baz:", "/foo/bar"]
https:/
app/assets/
namespace
Took me a second to agree, but yes, nice.
https:/
app/assets/
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Made changes around your suggestions, but will wait for other reviews
https:/
File app/assets/
https:/
app/assets/
(TARDIS).
On 2013/02/14 14:00:09, gary.poster wrote:
> lol
I forgot I left that in there. :)
https:/
app/assets/
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:/
app/assets/
url.split(
On 2013/02/14 14:00:09, gary.poster wrote:
> Might be helpful to include examples in comments:
> > '/foo/bar'
> ["/foo/bar"]
> '/> :baz:/foo/
> ["", ":baz:", "/foo/bar"]
Done.
https:/
app/assets/
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.defaultNam
- 374. By Benjamin Saller
-
review feedback
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
File app/app.js (right):
https:/
app/app.js:346: //console.
Remove commented code.
https:/
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:/
app/app.js:395: var loc = Y.getLocation();
Very minor: could be moved to top of function and used in line 391.
https:/
app/app.js:404: /**
We should note that this comes from App.Base, along with what has
changed.
https:/
app/app.js:996: * dispatches once per any dispatch call wrt namespace
with regards to
https:/
File app/assets/
https:/
app/assets/
Normalize, without
https:/
app/assets/
qs: querystring}.
Should be returning just the URL, correct?
- 375. By Benjamin Saller
-
review changes
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
thanks for the review, pushing the updates.
https:/
File app/app.js (right):
https:/
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:/
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:/
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:/
File app/assets/
https:/
app/assets/
On 2013/02/14 17:58:27, matthew.scott wrote:
> Normalize, without
Done.
https:/
app/assets/
qs: querystring}.
On 2013/02/14 17:58:27, matthew.scott wrote:
> Should be returning just the URL, correct?
Done.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
- 376. By Benjamin Saller
-
bad merge missed login-mask -> full-screen-mask rename
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
There was an issue with the login-mask renaming that I somehow missed in
the merge, resovled now.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
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:/
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:/
File app/app.js (right):
https:/
app/app.js:237: this._routeGene
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:/
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:/
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:/
app/app.js:351: Y.each(paths, function(p, ns) {
What is ns, and why don't you use it?
https:/
app/app.js:354: // to whom it applies.
Thorough thinking.
https:/
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...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jeff Pihach (hatch) wrote : | # |
This looks great! I just have a couple small comments :-)
https:/
File app/assets/
https:/
app/assets/
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(
}
https:/
app/assets/
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:/
app/assets/
refernce to same namespace');
s/refernce/
https:/
app/assets/
u is url? Please don't manually minify - use meaningful variable names
</pet peeve> :-)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
Just a quick FYI.
https:/
File app/assets/
https:/
app/assets/
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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 :-)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
Data:
* Trim is available in recent JS, but it only trims whitespace:
https:/
* JS does not have a regex escape function, but YUI does:
http://
* 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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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/
familiar to anyone who has reviewed this branch.
- 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
File app/assets/
https:/
app/assets/
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(
> >
> > }
> 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:/
app/assets/
refernce to same namespace');
On 2013/02/18 17:12:47, jeff.pihach wrote:
> s/refernce/
Done.
https:/
app/assets/
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benjamin Saller (bcsaller) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jeff Pihach (hatch) wrote : | # |
Looks good! I just wanted to add a comment for a future enhancement.
https:/
File app/app.js (right):
https:/
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 -
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
Land as is - looks good to me and works well. Thanks for the branch!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/urlFragme
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:/
Preview Diff
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" |
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 nt/:ns2: /etc.
support
for namespaced urls where these are defined as
/:ns:/urlFragme
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: javascripts/ routing. js debug.js routing. js
A [revision details]
M app/app.js
A app/assets/
M app/modules-
M bin/merge-files
M test/index.html
A test/test_
M undocumented