Merge lp:~hatch/juju-gui/bundle-detail-view into lp:juju-gui/experimental
- bundle-detail-view
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~hatch/juju-gui/bundle-detail-view |
Merge into: | lp:juju-gui/experimental |
Diff against target: |
1749 lines (+855/-797) 7 files modified
app/modules-debug.js (+1/-1) app/store/charm.js (+0/-769) app/store/charmworld.js (+798/-0) app/subapps/browser/browser.js (+1/-2) app/subapps/browser/views/charm.js (+53/-19) app/templates/bundle.handlebars (+2/-2) undocumented (+0/-4) |
To merge this branch: | bzr merge lp:~hatch/juju-gui/bundle-detail-view |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
WIP - Bundle details view
This is a WIP fist pass for the bundle details view. Because there is
no way to access this from the application you can access it by
using the following URL:
http://
Unfortunately the rename caused the diff to go wonky so the additions
in charmworld.js were the bundle and _bundle functions.
- 1100. By Jeff Pihach
-
renaming back to attempt to get a proper diff
- 1101. By Jeff Pihach
-
bzr rename
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jeff Pihach (hatch) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
I think this is exactly on the right path. I'd tweak a few things. I
think the View can be a bit simpler by dealing with a single ATTR for a
model instance or a token id and that logic should be pushed to the
store. That way all those decisions are made there.
We can chat about details later but looks good and thanks for working on
getting this into play.
https:/
File app/store/
https:/
app/store/
ns is already defined? why do we need juju.charmworld
https:/
app/store/
a bundle from Charmworld.
don't need "Public method" we already can tell that.
https:/
app/store/
bindScope) {
if you compare this to the charm call, there's some work we could do
here around the caching. We should add support for caching details as we
do in the charms so that subsequent calls will be instantly available.
The cache is maintained by the store instance I think as I know it's
shared around both Browser and main app code.
https:/
app/store/
bundle's details.
https:/
app/store/
defaultSeries) {
should we have a promiseBundle? This came after the original charm call
and maybe working with a promise from the start would be nice here?
https:/
app/store/
did you verify this works for bundle ids?
https:/
app/store/
{
any calls to this are hints that we need to look for the later check for
using the default charm icon and add a check for default bundle icon.
https:/
app/store/
bindScope) {
we'll need a bundle related method as well. Per the call with Gary
today, related bundles are lists of other bundles in the same basket as
the current one.
https:/
File app/subapps/
https:/
app/subapps/
I'm assuming existing tests pass with this change?
https:/
File app/subapps/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Jeff Pihach (hatch) wrote : | # |
Thanks for the review. Replies are below - lets chat in the morning and
then I will start a few real branches to tackle these changes.
https:/
File app/store/
https:/
app/store/
Not sure, this was from the old code.
https:/
app/store/
bindScope) {
Right, I wasn't sure how the cache was implemented so I left it out at
this point to get a functional example asap.
https:/
app/store/
defaultSeries) {
I agree, as I was reading through this stuff it would be great if we
only used the promises for charms and bundles.
https:/
app/store/
It does not so I removed it from the template - atm it looks like we
only have default icons, but will confirm in the am.
https:/
app/store/
bindScope) {
Hmm good call - that can be a follow-up
https:/
File app/subapps/
https:/
app/subapps/
You bet - I haven't manually checked the validity of all the paths yet
but all of the tests pass and qa was ok.
https:/
File app/subapps/
https:/
app/subapps/
Y.Base.
We were trying to come up with a good name :)
https:/
app/subapps/
function(bundle) {
It's definitely missing a lot of logic :) It's still a WIP - but we also
don't have UX for this view yet so any effort spent here might be wasted
- we can talk in the am.
https:/
app/subapps/
Yeah I had intended on renaming this assuming the impl made sense.
https:/
app/subapps/
I was also thinking of something similar but I didn't want to modify
that deep for this prototype
https:/
app/subapps/
Unmerged revisions
- 1101. By Jeff Pihach
-
bzr rename
- 1100. By Jeff Pihach
-
renaming back to attempt to get a proper diff
- 1099. By Jeff Pihach
-
added renamed file
- 1098. By Jeff Pihach
-
first pass at adding a bundle view handling
Preview Diff
1 | === modified file 'app/modules-debug.js' |
2 | --- app/modules-debug.js 2013-09-27 16:26:18 +0000 |
3 | +++ app/modules-debug.js 2013-09-30 21:13:03 +0000 |
4 | @@ -371,7 +371,7 @@ |
5 | }, |
6 | |
7 | 'juju-charm-store': { |
8 | - fullpath: '/juju-ui/store/charm.js' |
9 | + fullpath: '/juju-ui/store/charmworld.js' |
10 | }, |
11 | |
12 | 'juju-websocket-logging': { |
13 | |
14 | === removed file 'app/store/charm.js' |
15 | --- app/store/charm.js 2013-09-27 14:15:38 +0000 |
16 | +++ app/store/charm.js 1970-01-01 00:00:00 +0000 |
17 | @@ -1,769 +0,0 @@ |
18 | -/* |
19 | -This file is part of the Juju GUI, which lets users view and manage Juju |
20 | -environments within a graphical interface (https://launchpad.net/juju-gui). |
21 | -Copyright (C) 2012-2013 Canonical Ltd. |
22 | - |
23 | -This program is free software: you can redistribute it and/or modify it under |
24 | -the terms of the GNU Affero General Public License version 3, as published by |
25 | -the Free Software Foundation. |
26 | - |
27 | -This program is distributed in the hope that it will be useful, but WITHOUT |
28 | -ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
29 | -SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero |
30 | -General Public License for more details. |
31 | - |
32 | -You should have received a copy of the GNU Affero General Public License along |
33 | -with this program. If not, see <http://www.gnu.org/licenses/>. |
34 | -*/ |
35 | - |
36 | -'use strict'; |
37 | - |
38 | -/** |
39 | - * Provide the Charmworld API datastore class. |
40 | - * |
41 | - * @module store |
42 | - * @submodule store.charm |
43 | - */ |
44 | - |
45 | -YUI.add('juju-charm-store', function(Y) { |
46 | - var juju = Y.namespace('juju'), |
47 | - ns = Y.namespace('juju.charmworld'), |
48 | - models = Y.namespace('juju.models'); |
49 | - |
50 | - // Make Y.juju.charmworld refernce the "juju.charmworld" namespace. |
51 | - juju.charmworld = ns; |
52 | - |
53 | - ns.ApiHelper = Y.Base.create('ApiHelper', Y.Base, [], { |
54 | - |
55 | - /** |
56 | - Initializer for the class. |
57 | - |
58 | - @method initializer |
59 | - @param {Object} cfg The configuration for the API interface. |
60 | - @return {undefined} Nothing. |
61 | - */ |
62 | - initializer: function(cfg) { |
63 | - this.sendRequest = cfg.sendRequest; |
64 | - }, |
65 | - |
66 | - /** |
67 | - Send a request and handle the response from the API. |
68 | - |
69 | - @method makeRequest |
70 | - @param {Object} args any query params and arguments required. |
71 | - @return {undefined} Nothing. |
72 | - */ |
73 | - makeRequest: function(apiEndpoint, callbacks, args) { |
74 | - // Any query string args need to be put onto the endpoint for calling. |
75 | - if (args) { |
76 | - apiEndpoint = apiEndpoint + '?' + Y.QueryString.stringify(args); |
77 | - } |
78 | - |
79 | - this.sendRequest({ |
80 | - request: apiEndpoint, |
81 | - callback: { |
82 | - success: function(io_request) { |
83 | - var res = Y.JSON.parse( |
84 | - io_request.response.results[0].responseText |
85 | - ); |
86 | - callbacks.success(res); |
87 | - }, |
88 | - |
89 | - 'failure': function(io_request) { |
90 | - var respText = io_request.response.results[0].responseText, |
91 | - res; |
92 | - if (respText) { |
93 | - res = Y.JSON.parse(respText); |
94 | - } |
95 | - callbacks.failure(res, io_request); |
96 | - } |
97 | - } |
98 | - }); |
99 | - }, |
100 | - |
101 | - /** |
102 | - Normalize a charm name so we can request its full data. Charm lookup |
103 | - requires a very specific form of the charm identifier. |
104 | - |
105 | - series/charm-revision |
106 | - |
107 | - where revision can currently (API v2) be any numeric placeholder. |
108 | - |
109 | - @method normalizeCharmId |
110 | - @param {String} charmId to normalize. |
111 | - @param {String} [defaultSeries='precise'] The series to use if none is |
112 | - specified in the charm ID. |
113 | - @return {String} normalized id. |
114 | - */ |
115 | - normalizeCharmId: function(charmId, defaultSeries) { |
116 | - var result = charmId; |
117 | - if (/^(cs:|local:)/.exec(result)) { |
118 | - result = result.slice(result.indexOf(':') + 1); |
119 | - } |
120 | - |
121 | - if (result.indexOf('/') === -1) { |
122 | - if (!defaultSeries) { |
123 | - console.warn('No default series provided when normalizing charm ' + |
124 | - 'ID. Using "precise".'); |
125 | - defaultSeries = 'precise'; |
126 | - } |
127 | - result = defaultSeries + '/' + result; |
128 | - } |
129 | - return result; |
130 | - } |
131 | - |
132 | - }, { |
133 | - ATTRS: { |
134 | - } |
135 | - }); |
136 | - |
137 | - /** |
138 | - * Charmworld API version 3 interface. |
139 | - * |
140 | - * @class APIv3 |
141 | - * @extends {Base} |
142 | - * |
143 | - */ |
144 | - ns.APIv3 = Y.Base.create('APIv3', Y.Base, [], { |
145 | - _apiRoot: 'api/3', |
146 | - |
147 | - /** |
148 | - * Send the actual request and handle response from the API. |
149 | - * |
150 | - * @method _makeRequest |
151 | - * @param {Object} args any query params and arguments required. |
152 | - * @private |
153 | - * |
154 | - */ |
155 | - _makeRequest: function(apiEndpoint, callbacks, args) { |
156 | - // If we're in the noop state, just call the error callback. |
157 | - if (this.get('noop')) { |
158 | - callbacks.failure('noop failure'); |
159 | - return; |
160 | - } |
161 | - // Delegate the request making to the helper object. |
162 | - this.apiHelper.makeRequest(apiEndpoint, callbacks, args); |
163 | - }, |
164 | - |
165 | - /** |
166 | - * API call to fetch autocomplete suggestions based on the current term. |
167 | - * |
168 | - * @method autocomplete |
169 | - * @param {Object} query the filters data object for search. |
170 | - * @param {Object} filters the filters data object for search. |
171 | - * @param {Object} callbacks the success/failure callbacks to use. |
172 | - * @param {Object} bindScope the scope of *this* in the callbacks. |
173 | - */ |
174 | - autocomplete: function(filters, callbacks, bindScope) { |
175 | - var endpoint = 'search'; |
176 | - // Force that this is an autocomplete call to perform matching on the |
177 | - // start of names vs a fulltext search. |
178 | - filters.autocomplete = 'true'; |
179 | - filters.limit = 5; |
180 | - if (bindScope) { |
181 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
182 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
183 | - } |
184 | - this._makeRequest(endpoint, callbacks, filters); |
185 | - }, |
186 | - |
187 | - |
188 | - /** |
189 | - * API call to fetch a charm's details. |
190 | - * |
191 | - * @method charm |
192 | - * @param {String} charmID the charm to fetch. |
193 | - * @param {Object} callbacks the success/failure callbacks to use. |
194 | - * @param {Object} bindScope the scope of *this* in the callbacks. |
195 | - * |
196 | - */ |
197 | - _charm: function(charmID, callbacks, bindScope) { |
198 | - var endpoint = 'charm/' + charmID; |
199 | - if (bindScope) { |
200 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
201 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
202 | - } |
203 | - |
204 | - this._makeRequest(endpoint, callbacks); |
205 | - }, |
206 | - |
207 | - /** |
208 | - * API call to fetch a charm's details, with an optional local cache. |
209 | - * |
210 | - * @method charmWithCache |
211 | - * @param {String} charmID The charm to fetch This is the fully qualified |
212 | - * charm name in the format scheme:series/charm-revision. |
213 | - * @param {Object} callbacks The success/failure callbacks to use. |
214 | - * @param {Object} bindScope The scope of "this" in the callbacks. |
215 | - * @param {ModelList} [cache] a local cache of browser charms. |
216 | - * @param {String} [defaultSeries='precise'] The series to use if none is |
217 | - * specified in the charm ID. |
218 | - */ |
219 | - charm: function(charmID, callbacks, bindScope, cache, defaultSeries) { |
220 | - if (bindScope) { |
221 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
222 | - } |
223 | - if (cache) { |
224 | - var charm = cache.getById(charmID); |
225 | - if (charm) { |
226 | - // If the charm was found in the cache, then we can declare success |
227 | - // without ever making a request to charmworld. |
228 | - Y.soon(function() { |
229 | - // Since there wasn't really a request, there is no data, so we |
230 | - // pass an empty object as the "data" parameter. |
231 | - callbacks.success({}, charm); |
232 | - }); |
233 | - return; |
234 | - } else { |
235 | - var successCB = callbacks.success; |
236 | - callbacks.success = function(data) { |
237 | - var charm = new Y.juju.models.Charm(data.charm); |
238 | - if (data.metadata) { |
239 | - charm.set('metadata', data.metadata); |
240 | - } |
241 | - cache.add(charm); |
242 | - successCB(data, charm); |
243 | - }; |
244 | - } |
245 | - } |
246 | - this._charm(this.apiHelper.normalizeCharmId(charmID, defaultSeries), |
247 | - callbacks, bindScope); |
248 | - }, |
249 | - |
250 | - /** |
251 | - Like the "charm" method but returning a Promise. |
252 | - |
253 | - @method promiseCharm |
254 | - @param {String} charmId The ID of the charm to fetch. |
255 | - @param {ModelList} cache A local cache of browser charms. |
256 | - @param {String} [defaultSeries='precise'] The series to use if none is |
257 | - specified in the charm ID. |
258 | - @return {Promise} Returns a promise. Triggered with the result of calling |
259 | - this.charm. |
260 | - */ |
261 | - promiseCharm: function(charmId, cache, defaultSeries) { |
262 | - var self = this; |
263 | - return Y.Promise(function(resolve, reject) { |
264 | - self.charm(charmId, { 'success': resolve, 'failure': reject }, |
265 | - self, cache, defaultSeries); |
266 | - }); |
267 | - }, |
268 | - |
269 | - /** |
270 | - Promises to return the latest charm ID for a given charm if a newer one |
271 | - exists; this also caches the newer charm if one is available. |
272 | - |
273 | - @method promiseUpgradeAvailability |
274 | - @param {Charm} charm An existing charm potentially in need of an upgrade. |
275 | - @param {ModelList} cache A local cache of browser charms. |
276 | - @return {Promise} A promise for a newer charm ID or undefined. |
277 | - */ |
278 | - promiseUpgradeAvailability: function(charm, cache) { |
279 | - // Get the charm's store ID, then remove the version number to retrieve |
280 | - // the latest version of the charm. |
281 | - var storeId, revision; |
282 | - if (charm instanceof Y.Model) { |
283 | - storeId = charm.get('storeId'); |
284 | - revision = parseInt(charm.get('revision'), 10); |
285 | - } else { |
286 | - storeId = charm.url; |
287 | - revision = parseInt(charm.revision, 10); |
288 | - } |
289 | - storeId = storeId.replace(/-\d+$/, ''); |
290 | - // XXX By using a cache we hide charm versions that have become available |
291 | - // since we last requested the most recent version. |
292 | - return this.promiseCharm(storeId, cache) |
293 | - .then(function(latest) { |
294 | - var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10); |
295 | - if (latestVersion > revision) { |
296 | - return latest.charm.id; |
297 | - } |
298 | - }, function(e) { |
299 | - throw e; |
300 | - }); |
301 | - }, |
302 | - |
303 | - /** |
304 | - * API call to search charms |
305 | - * |
306 | - * @method search |
307 | - * @param {Object} filters the filters data object for search. |
308 | - * @param {Object} callbacks the success/failure callbacks to use. |
309 | - * @param {Object} bindScope the scope of *this* in the callbacks. |
310 | - */ |
311 | - search: function(filters, callbacks, bindScope) { |
312 | - var endpoint = 'search'; |
313 | - if (bindScope) { |
314 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
315 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
316 | - } |
317 | - this._makeRequest(endpoint, callbacks, filters); |
318 | - }, |
319 | - |
320 | - /** |
321 | - * Fetch the contents of a charm's file. |
322 | - * |
323 | - * @method file |
324 | - * @param {String} charmID The id of the charm's file we want. |
325 | - * @param {String} filename The path/name of the file to fetch content. |
326 | - * @param {Object} callbacks The success/failure callbacks. |
327 | - * @param {Object} bindScope The scope for this in the callbacks. |
328 | - * |
329 | - */ |
330 | - file: function(charmID, filename, callbacks, bindScope) { |
331 | - // If we're in the noop state, just call the error callback. |
332 | - if (this.get('noop')) { |
333 | - callbacks.failure('noop failure'); |
334 | - return; |
335 | - } |
336 | - |
337 | - var endpoint = 'charm/' + charmID + '/file/' + filename; |
338 | - if (bindScope) { |
339 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
340 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
341 | - } |
342 | - |
343 | - this.get('datasource').sendRequest({ |
344 | - request: endpoint, |
345 | - callback: { |
346 | - success: function(io_request) { |
347 | - callbacks.success(io_request.response.results[0].responseText); |
348 | - }, |
349 | - 'failure': function(io_request) { |
350 | - var respText = io_request.response.results[0].responseText, |
351 | - res; |
352 | - if (respText) { |
353 | - res = Y.JSON.parse(respText); |
354 | - } |
355 | - callbacks.failure(res, io_request); |
356 | - } |
357 | - } |
358 | - }); |
359 | - }, |
360 | - |
361 | - /** |
362 | - Generate the API path to a charm icon. |
363 | - This is useful when generating links and references in HTML to the |
364 | - charm's icon and is constructing the correct icon based on reviewed |
365 | - status and categories on the charm. |
366 | - |
367 | - @method iconpath |
368 | - @param {String} charmID The id of the charm to grab the icon for. |
369 | - @return {String} The URL of the charm's icon. |
370 | - */ |
371 | - iconpath: function(charmID) { |
372 | - // If this is a local charm, then we need use a hard coded path to the |
373 | - // default icon since we cannot fetch its category data or its own |
374 | - // icon. |
375 | - // XXX: #1202703 - this is a short term fix for the bug. Need longer |
376 | - // term solution. |
377 | - if (charmID.indexOf('local:') === 0) { |
378 | - return this.get('apiHost') + |
379 | - 'static/img/charm_160.svg'; |
380 | - |
381 | - } else { |
382 | - // Get the charm ID from the service. In some cases, this will be |
383 | - // the charm URL with a protocol, which will need to be removed. |
384 | - // The following regular expression removes everything up to the |
385 | - // colon portion of the quote and leaves behind a charm ID. |
386 | - charmID = charmID.replace(/^[^:]+:/, ''); |
387 | - return this.get('apiHost') + [ |
388 | - this._apiRoot, |
389 | - 'charm', |
390 | - charmID, |
391 | - 'file', |
392 | - 'icon.svg'].join('/'); |
393 | - } |
394 | - }, |
395 | - |
396 | - /** |
397 | - * Generate the url to an icon for the category specified. |
398 | - * |
399 | - * @method buildCategoryIconPath |
400 | - * @param {String} categoryID the id of the category to load an icon for. |
401 | - * |
402 | - */ |
403 | - buildCategoryIconPath: function(categoryID) { |
404 | - return [ |
405 | - this.get('apiHost'), |
406 | - 'static/img/category-', |
407 | - categoryID, |
408 | - '-bw.svg' |
409 | - ].join(''); |
410 | - }, |
411 | - |
412 | - /** |
413 | - * Load the QA data for a specific charm. |
414 | - * |
415 | - * @method qa |
416 | - * @param {String} charmID The charm to fetch QA data for. |
417 | - * @param {Object} callbacks The success/failure callbacks to use. |
418 | - * @param {Object} bindScope The scope for 'this' in the callbacks. |
419 | - * |
420 | - */ |
421 | - qa: function(charmID, callbacks, bindScope) { |
422 | - var endpoint = 'charm/' + charmID + '/qa'; |
423 | - if (bindScope) { |
424 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
425 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
426 | - } |
427 | - this._makeRequest(endpoint, callbacks); |
428 | - }, |
429 | - |
430 | - /** |
431 | - * Given a result list, append metadata is appended to the charm or bundle |
432 | - * as the 'metadata' attribute and convert to objects. |
433 | - * |
434 | - * @method transformResults |
435 | - * @param {Object} JSON decoded data from response. |
436 | - * @return {Array} List of charm and bundle objects. |
437 | - * |
438 | - */ |
439 | - transformResults: function(data) { |
440 | - // Append the metadata to the actual token object. |
441 | - var token; |
442 | - var tokens; |
443 | - tokens = Y.Array.map(data, function(entity) { |
444 | - if (Y.Lang.isValue(entity.charm)) { |
445 | - token = new Y.juju.models.Charm(entity.charm); |
446 | - } else { |
447 | - token = new Y.juju.models.Bundle(entity.bundle); |
448 | - } |
449 | - if (entity.metadata) { |
450 | - token.set('metadata', entity.metadata); |
451 | - } |
452 | - return token; |
453 | - }); |
454 | - return tokens; |
455 | - }, |
456 | - |
457 | - /** |
458 | - * Initialize the API helper. Constructs a reusable datasource for all |
459 | - * calls. |
460 | - * |
461 | - * @method initializer |
462 | - * @param {Object} cfg configuration object. |
463 | - * |
464 | - */ |
465 | - initializer: function(cfg) { |
466 | - // XXX This isn't set on initial load so we have to manually hit the |
467 | - // setter to get datasource filled in. Must be a better way. |
468 | - this.set('apiHost', cfg.apiHost); |
469 | - }, |
470 | - |
471 | - /** |
472 | - * Fetch the interesting landing content from the charmworld API. |
473 | - * |
474 | - * @method interesting |
475 | - * @return {Object} data loaded from the API call. |
476 | - * |
477 | - */ |
478 | - interesting: function(callbacks, bindScope) { |
479 | - if (bindScope) { |
480 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
481 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
482 | - } |
483 | - |
484 | - this._makeRequest('search/interesting', callbacks); |
485 | - }, |
486 | - |
487 | - /** |
488 | - Fetch the related charm info from the charmworld API. |
489 | - |
490 | - @method related |
491 | - @param {String} charmID The charm to find related charms for. |
492 | - @param {Object} callbacks The success/failure callbacks to use. |
493 | - @param {Object} bindscope An object scope to perform callbacks in. |
494 | - @return {Object} data loaded from the API call. |
495 | - |
496 | - */ |
497 | - related: function(charmID, callbacks, bindScope) { |
498 | - var endpoint = 'charm/' + charmID + '/related'; |
499 | - if (bindScope) { |
500 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
501 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
502 | - } |
503 | - this._makeRequest(endpoint, callbacks); |
504 | - } |
505 | - }, { |
506 | - ATTRS: { |
507 | - /** |
508 | - * Required attribute for the host to talk to for API calls. |
509 | - * |
510 | - * @attribute apiHost |
511 | - * @default undefined |
512 | - * @type {String} |
513 | - * |
514 | - */ |
515 | - apiHost: { |
516 | - required: true, |
517 | - setter: function(val) { |
518 | - if (val && !val.match(/\/$/)) { |
519 | - val = val + '/'; |
520 | - } |
521 | - // Make sure we update the datasource if our apiHost changes. |
522 | - var source = val + this._apiRoot + '/'; |
523 | - this.set('datasource', new Y.DataSource.IO({ source: source })); |
524 | - return val; |
525 | - } |
526 | - }, |
527 | - |
528 | - /** |
529 | - * Auto constructed datasource object based on the apiHost attribute. |
530 | - * @attribute datasource |
531 | - * @type {Datasource} |
532 | - * |
533 | - */ |
534 | - datasource: { |
535 | - setter: function(datasource) { |
536 | - // Construct an API helper using the new datasource. |
537 | - this.apiHelper = new ns.ApiHelper({ |
538 | - sendRequest: Y.bind(datasource.sendRequest, datasource) |
539 | - }); |
540 | - return datasource; |
541 | - } |
542 | - }, |
543 | - |
544 | - /** |
545 | - If there's no config we end up setting noop on the store so that tests |
546 | - that don't need to worry about the browser can safely ignore it. |
547 | - |
548 | - We do log a console error, so those will occur on these tests to help |
549 | - make it easy to catch an issue when you don't mean to noop the store. |
550 | - |
551 | - @attribute noop |
552 | - @default false |
553 | - @type {Boolean} |
554 | - |
555 | - */ |
556 | - noop: { |
557 | - value: false |
558 | - } |
559 | - } |
560 | - }); |
561 | - |
562 | - /** |
563 | - * Charmworld API version 2 interface. |
564 | - * |
565 | - * @class APIv2 |
566 | - * @extends {APIv3} |
567 | - * |
568 | - * This class inherits from the v3 version of the API so that removing v2 |
569 | - * once it is no longer needed will be easy (just delete this class, the one |
570 | - * or two places it is referenced in the code, and its associated tests). |
571 | - * |
572 | - */ |
573 | - ns.APIv2 = Y.Base.create('APIv2', ns.APIv3, [], { |
574 | - _apiRoot: 'api/2', |
575 | - |
576 | - /** |
577 | - * API call to fetch autocomplete suggestions based on the current term. |
578 | - * |
579 | - * @method autocomplete |
580 | - * @param {Object} query the filters data object for search. |
581 | - * @param {Object} filters the filters data object for search. |
582 | - * @param {Object} callbacks the success/failure callbacks to use. |
583 | - * @param {Object} bindScope the scope of *this* in the callbacks. |
584 | - */ |
585 | - autocomplete: function(filters, callbacks, bindScope) { |
586 | - var endpoint = 'charms'; |
587 | - // Force that this is an autocomplete call to perform matching on the |
588 | - // start of names vs a fulltext search. |
589 | - filters.autocomplete = 'true'; |
590 | - filters.limit = 5; |
591 | - if (bindScope) { |
592 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
593 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
594 | - } |
595 | - this._makeRequest(endpoint, callbacks, filters); |
596 | - }, |
597 | - |
598 | - /** |
599 | - * API call to fetch a charm's details, with an optional local cache. |
600 | - * |
601 | - * @method charmWithCache |
602 | - * @param {String} charmID The charm to fetch This is the fully qualified |
603 | - * charm name in the format scheme:series/charm-revision. |
604 | - * @param {Object} callbacks The success/failure callbacks to use. |
605 | - * @param {Object} bindScope The scope of "this" in the callbacks. |
606 | - * @param {ModelList} [cache] a local cache of browser charms. |
607 | - * @param {String} [defaultSeries='precise'] The series to use if none is |
608 | - * specified in the charm ID. |
609 | - */ |
610 | - charm: function(charmID, callbacks, bindScope, cache, defaultSeries) { |
611 | - if (bindScope) { |
612 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
613 | - } |
614 | - if (cache) { |
615 | - var charm = cache.getById(charmID); |
616 | - if (charm) { |
617 | - // If the charm was found in the cache, then we can declare success |
618 | - // without ever making a request to charmworld. |
619 | - Y.soon(function() { |
620 | - // Since there wasn't really a request, there is no data, so we |
621 | - // pass an empty object as the "data" parameter. |
622 | - callbacks.success({}, charm); |
623 | - }); |
624 | - return; |
625 | - } else { |
626 | - var successCB = callbacks.success; |
627 | - callbacks.success = function(data) { |
628 | - var charm = new Y.juju.models.Charm(data.charm); |
629 | - if (data.metadata) { |
630 | - charm.set('metadata', data.metadata); |
631 | - } |
632 | - cache.add(charm); |
633 | - successCB(data, charm); |
634 | - }; |
635 | - } |
636 | - } |
637 | - charmID = this.apiHelper.normalizeCharmId(charmID, defaultSeries); |
638 | - // If the charm ID does not have a revision number (or "HEAD"), add one. |
639 | - if (/\-(\d+|HEAD)/.exec(charmID) === null) { |
640 | - // Add in a revision placeholder. Any value will do, v2 of the |
641 | - // charmworld API ignores revision numbers. |
642 | - charmID = charmID + '-1'; |
643 | - } |
644 | - this._charm(charmID, callbacks, bindScope); |
645 | - }, |
646 | - |
647 | - /** |
648 | - Promises to return the latest charm ID for a given charm if a newer one |
649 | - exists; this also caches the newer charm if one is available. |
650 | - |
651 | - @method promiseUpgradeAvailability |
652 | - @param {Charm} charm An existing charm potentially in need of an upgrade. |
653 | - @param {ModelList} cache A local cache of browser charms. |
654 | - @return {Promise} A promise for a newer charm ID or undefined. |
655 | - */ |
656 | - promiseUpgradeAvailability: function(charm, cache) { |
657 | - // Get the charm's store ID, then replace the version number |
658 | - // with '-HEAD' to retrieve the latest version of the charm. |
659 | - var storeId, revision; |
660 | - if (charm instanceof Y.Model) { |
661 | - storeId = charm.get('storeId'); |
662 | - revision = parseInt(charm.get('revision'), 10); |
663 | - } else { |
664 | - storeId = charm.url; |
665 | - revision = parseInt(charm.revision, 10); |
666 | - } |
667 | - storeId = storeId.replace(/-\d+$/, '-HEAD'); |
668 | - // XXX By using a cache we hide charm versions that have become available |
669 | - // since we last requested the most recent version. |
670 | - return this.promiseCharm(storeId, cache) |
671 | - .then(function(latest) { |
672 | - var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10); |
673 | - if (latestVersion > revision) { |
674 | - return latest.charm.id; |
675 | - } |
676 | - }, function(e) { |
677 | - throw e; |
678 | - }); |
679 | - }, |
680 | - |
681 | - /** |
682 | - * API call to search charms |
683 | - * |
684 | - * @method search |
685 | - * @param {Object} filters the filters data object for search. |
686 | - * @param {Object} callbacks the success/failure callbacks to use. |
687 | - * @param {Object} bindScope the scope of *this* in the callbacks. |
688 | - */ |
689 | - search: function(filters, callbacks, bindScope) { |
690 | - var endpoint = 'charms'; |
691 | - if (bindScope) { |
692 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
693 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
694 | - } |
695 | - this._makeRequest(endpoint, callbacks, filters); |
696 | - }, |
697 | - |
698 | - /** |
699 | - Generate the API path to a charm icon. |
700 | - This is useful when generating links and references in HTML to the |
701 | - charm's icon and is constructing the correct icon based on reviewed |
702 | - status and categories on the charm. |
703 | - |
704 | - @method iconpath |
705 | - @param {String} charmID The id of the charm to grab the icon for. |
706 | - @return {String} The URL of the charm's icon. |
707 | - */ |
708 | - iconpath: function(charmID) { |
709 | - // If this is a local charm, then we need use a hard coded path to the |
710 | - // default icon since we cannot fetch its category data or its own |
711 | - // icon. |
712 | - // XXX: #1202703 - this is a short term fix for the bug. Need longer |
713 | - // term solution. |
714 | - if (charmID.indexOf('local:') === 0) { |
715 | - return this.get('apiHost') + |
716 | - 'static/img/charm_160.svg'; |
717 | - |
718 | - } else { |
719 | - // Get the charm ID from the service. In some cases, this will be |
720 | - // the charm URL with a protocol, which will need to be removed. |
721 | - // The following regular expression removes everything up to the |
722 | - // colon portion of the quote and leaves behind a charm ID. |
723 | - charmID = charmID.replace(/^[^:]+:/, ''); |
724 | - return this.get('apiHost') + [ |
725 | - this._apiRoot, |
726 | - 'charm', |
727 | - charmID, |
728 | - 'icon.svg'].join('/'); |
729 | - } |
730 | - }, |
731 | - |
732 | - /** |
733 | - * Given a result list, turn that into an array of charm objects for the |
734 | - * application to use. Metadata is appended to the charm or bundle as the |
735 | - * 'metadata' attribute. |
736 | - * |
737 | - * @method transformResults |
738 | - * @param {Object} JSON decoded data from response. |
739 | - * @return {Array} List of charm objects. |
740 | - * |
741 | - */ |
742 | - transformResults: function(data) { |
743 | - // Remove non-charms (bundles) from the data. |
744 | - data = Y.Array.filter(data, function(charmData) { |
745 | - return Y.Lang.isValue(charmData.charm); |
746 | - }); |
747 | - // Append the metadata to the actual charm object. |
748 | - data = Y.Array.map(data, function(charmData) { |
749 | - var charm = new Y.juju.models.Charm(charmData.charm); |
750 | - if (charmData.metadata) { |
751 | - charm.set('metadata', charmData.metadata); |
752 | - } |
753 | - return charm; |
754 | - }); |
755 | - return data; |
756 | - }, |
757 | - |
758 | - /** |
759 | - * Fetch the interesting landing content from the charmworld API. |
760 | - * |
761 | - * @method interesting |
762 | - * @return {Object} data loaded from the API call. |
763 | - * |
764 | - */ |
765 | - interesting: function(callbacks, bindScope) { |
766 | - if (bindScope) { |
767 | - callbacks.success = Y.bind(callbacks.success, bindScope); |
768 | - callbacks.failure = Y.bind(callbacks.failure, bindScope); |
769 | - } |
770 | - |
771 | - this._makeRequest('charms/interesting', callbacks); |
772 | - } |
773 | - }, { |
774 | - ATTRS: {} |
775 | - }); |
776 | - |
777 | -}, '0.1.0', { |
778 | - requires: [ |
779 | - 'datasource-io', |
780 | - 'json-parse', |
781 | - 'juju-charm-models', |
782 | - 'juju-bundle-models', |
783 | - 'promise', |
784 | - 'querystring-stringify' |
785 | - ] |
786 | -}); |
787 | |
788 | === added file 'app/store/charmworld.js' |
789 | --- app/store/charmworld.js 1970-01-01 00:00:00 +0000 |
790 | +++ app/store/charmworld.js 2013-09-30 21:13:03 +0000 |
791 | @@ -0,0 +1,798 @@ |
792 | +/* |
793 | +This file is part of the Juju GUI, which lets users view and manage Juju |
794 | +environments within a graphical interface (https://launchpad.net/juju-gui). |
795 | +Copyright (C) 2012-2013 Canonical Ltd. |
796 | + |
797 | +This program is free software: you can redistribute it and/or modify it under |
798 | +the terms of the GNU Affero General Public License version 3, as published by |
799 | +the Free Software Foundation. |
800 | + |
801 | +This program is distributed in the hope that it will be useful, but WITHOUT |
802 | +ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
803 | +SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero |
804 | +General Public License for more details. |
805 | + |
806 | +You should have received a copy of the GNU Affero General Public License along |
807 | +with this program. If not, see <http://www.gnu.org/licenses/>. |
808 | +*/ |
809 | + |
810 | +'use strict'; |
811 | + |
812 | +/** |
813 | + * Provide the Charmworld API datastore class. |
814 | + * |
815 | + * @module store |
816 | + * @submodule store.charm |
817 | + */ |
818 | + |
819 | +YUI.add('juju-charm-store', function(Y) { |
820 | + var juju = Y.namespace('juju'), |
821 | + ns = Y.namespace('juju.charmworld'), |
822 | + models = Y.namespace('juju.models'); |
823 | + |
824 | + // Make Y.juju.charmworld reference the "juju.charmworld" namespace. |
825 | + juju.charmworld = ns; |
826 | + |
827 | + ns.ApiHelper = Y.Base.create('ApiHelper', Y.Base, [], { |
828 | + |
829 | + /** |
830 | + Initializer for the class. |
831 | + |
832 | + @method initializer |
833 | + @param {Object} cfg The configuration for the API interface. |
834 | + @return {undefined} Nothing. |
835 | + */ |
836 | + initializer: function(cfg) { |
837 | + this.sendRequest = cfg.sendRequest; |
838 | + }, |
839 | + |
840 | + /** |
841 | + Send a request and handle the response from the API. |
842 | + |
843 | + @method makeRequest |
844 | + @param {Object} args any query params and arguments required. |
845 | + @return {undefined} Nothing. |
846 | + */ |
847 | + makeRequest: function(apiEndpoint, callbacks, args) { |
848 | + // Any query string args need to be put onto the endpoint for calling. |
849 | + if (args) { |
850 | + apiEndpoint = apiEndpoint + '?' + Y.QueryString.stringify(args); |
851 | + } |
852 | + |
853 | + this.sendRequest({ |
854 | + request: apiEndpoint, |
855 | + callback: { |
856 | + 'success': function(io_request) { |
857 | + var res = Y.JSON.parse( |
858 | + io_request.response.results[0].responseText |
859 | + ); |
860 | + callbacks.success(res); |
861 | + }, |
862 | + |
863 | + 'failure': function(io_request) { |
864 | + var respText = io_request.response.results[0].responseText, |
865 | + res; |
866 | + if (respText) { |
867 | + res = Y.JSON.parse(respText); |
868 | + } |
869 | + callbacks.failure(res, io_request); |
870 | + } |
871 | + } |
872 | + }); |
873 | + }, |
874 | + |
875 | + /** |
876 | + Normalize a charm name so we can request its full data. Charm lookup |
877 | + requires a very specific form of the charm identifier. |
878 | + |
879 | + series/charm-revision |
880 | + |
881 | + where revision can currently (API v2) be any numeric placeholder. |
882 | + |
883 | + @method normalizeCharmId |
884 | + @param {String} charmId to normalize. |
885 | + @param {String} [defaultSeries='precise'] The series to use if none is |
886 | + specified in the charm ID. |
887 | + @return {String} normalized id. |
888 | + */ |
889 | + normalizeCharmId: function(charmId, defaultSeries) { |
890 | + var result = charmId; |
891 | + if (/^(cs:|local:)/.exec(result)) { |
892 | + result = result.slice(result.indexOf(':') + 1); |
893 | + } |
894 | + |
895 | + if (result.indexOf('/') === -1) { |
896 | + if (!defaultSeries) { |
897 | + console.warn('No default series provided when normalizing charm ' + |
898 | + 'ID. Using "precise".'); |
899 | + defaultSeries = 'precise'; |
900 | + } |
901 | + result = defaultSeries + '/' + result; |
902 | + } |
903 | + return result; |
904 | + } |
905 | + |
906 | + }, { |
907 | + ATTRS: { |
908 | + } |
909 | + }); |
910 | + |
911 | + /** |
912 | + * Charmworld API version 3 interface. |
913 | + * |
914 | + * @class APIv3 |
915 | + * @extends {Base} |
916 | + * |
917 | + */ |
918 | + ns.APIv3 = Y.Base.create('APIv3', Y.Base, [], { |
919 | + _apiRoot: 'api/3', |
920 | + |
921 | + /** |
922 | + * Send the actual request and handle response from the API. |
923 | + * |
924 | + * @method _makeRequest |
925 | + * @param {Object} args any query params and arguments required. |
926 | + * @private |
927 | + * |
928 | + */ |
929 | + _makeRequest: function(apiEndpoint, callbacks, args) { |
930 | + // If we're in the noop state, just call the error callback. |
931 | + if (this.get('noop')) { |
932 | + callbacks.failure('noop failure'); |
933 | + return; |
934 | + } |
935 | + // Delegate the request making to the helper object. |
936 | + this.apiHelper.makeRequest(apiEndpoint, callbacks, args); |
937 | + }, |
938 | + |
939 | + /** |
940 | + * API call to fetch autocomplete suggestions based on the current term. |
941 | + * |
942 | + * @method autocomplete |
943 | + * @param {Object} query the filters data object for search. |
944 | + * @param {Object} filters the filters data object for search. |
945 | + * @param {Object} callbacks the success/failure callbacks to use. |
946 | + * @param {Object} bindScope the scope of *this* in the callbacks. |
947 | + */ |
948 | + autocomplete: function(filters, callbacks, bindScope) { |
949 | + var endpoint = 'search'; |
950 | + // Force that this is an autocomplete call to perform matching on the |
951 | + // start of names vs a fulltext search. |
952 | + filters.autocomplete = 'true'; |
953 | + filters.limit = 5; |
954 | + if (bindScope) { |
955 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
956 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
957 | + } |
958 | + this._makeRequest(endpoint, callbacks, filters); |
959 | + }, |
960 | + |
961 | + |
962 | + /** |
963 | + * API call to fetch a charm's details. |
964 | + * |
965 | + * @method charm |
966 | + * @param {String} charmID the charm to fetch. |
967 | + * @param {Object} callbacks the success/failure callbacks to use. |
968 | + * @param {Object} bindScope the scope of *this* in the callbacks. |
969 | + * |
970 | + */ |
971 | + _charm: function(charmID, callbacks, bindScope) { |
972 | + var endpoint = 'charm/' + charmID; |
973 | + if (bindScope) { |
974 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
975 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
976 | + } |
977 | + |
978 | + this._makeRequest(endpoint, callbacks); |
979 | + }, |
980 | + |
981 | + /** |
982 | + * API call to fetch a charm's details, with an optional local cache. |
983 | + * |
984 | + * @method charm |
985 | + * @param {String} charmID The charm to fetch This is the fully qualified |
986 | + * charm name in the format scheme:series/charm-revision. |
987 | + * @param {Object} callbacks The success/failure callbacks to use. |
988 | + * @param {Object} bindScope The scope of "this" in the callbacks. |
989 | + * @param {ModelList} [cache] a local cache of browser charms. |
990 | + * @param {String} [defaultSeries='precise'] The series to use if none is |
991 | + * specified in the charm ID. |
992 | + */ |
993 | + charm: function(charmID, callbacks, bindScope, cache, defaultSeries) { |
994 | + if (bindScope) { |
995 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
996 | + } |
997 | + if (cache) { |
998 | + var charm = cache.getById(charmID); |
999 | + if (charm) { |
1000 | + // If the charm was found in the cache, then we can declare success |
1001 | + // without ever making a request to charmworld. |
1002 | + Y.soon(function() { |
1003 | + // Since there wasn't really a request, there is no data, so we |
1004 | + // pass an empty object as the "data" parameter. |
1005 | + callbacks.success({}, charm); |
1006 | + }); |
1007 | + return; |
1008 | + } else { |
1009 | + var successCB = callbacks.success; |
1010 | + callbacks.success = function(data) { |
1011 | + var charm = new Y.juju.models.Charm(data.charm); |
1012 | + if (data.metadata) { |
1013 | + charm.set('metadata', data.metadata); |
1014 | + } |
1015 | + cache.add(charm); |
1016 | + successCB(data, charm); |
1017 | + }; |
1018 | + } |
1019 | + } |
1020 | + this._charm(this.apiHelper.normalizeCharmId(charmID, defaultSeries), |
1021 | + callbacks, bindScope); |
1022 | + }, |
1023 | + |
1024 | + /** |
1025 | + Public method to make an API call to fetch a bundle from Charmworld. |
1026 | + |
1027 | + @method bundle |
1028 | + @param {String} bundleID The bundle to fetch This is the fully qualified |
1029 | + bundle id. |
1030 | + @param {Object} callbacks The success/failure callbacks to use. |
1031 | + @param {Object} bindScope The scope of "this" in the callbacks. |
1032 | + */ |
1033 | + bundle: function(bundleID, callbacks, bindScope) { |
1034 | + this._bundle(bundleID, callbacks, bindScope); |
1035 | + }, |
1036 | + |
1037 | + /** |
1038 | + API call to fetch a charm's details. |
1039 | + |
1040 | + @method _bundle |
1041 | + @param {String} bundleID the bundle to fetch. |
1042 | + @param {Object} callbacks the success/failure callbacks to use. |
1043 | + @param {Object} bindScope the scope of *this* in the callbacks. |
1044 | + */ |
1045 | + _bundle: function(bundleID, callbacks, bindScope) { |
1046 | + if (bindScope) { |
1047 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1048 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1049 | + } |
1050 | + this._makeRequest(bundleID, callbacks); |
1051 | + }, |
1052 | + |
1053 | + /** |
1054 | + Like the "charm" method but returning a Promise. |
1055 | + |
1056 | + @method promiseCharm |
1057 | + @param {String} charmId The ID of the charm to fetch. |
1058 | + @param {ModelList} cache A local cache of browser charms. |
1059 | + @param {String} [defaultSeries='precise'] The series to use if none is |
1060 | + specified in the charm ID. |
1061 | + @return {Promise} Returns a promise. Triggered with the result of calling |
1062 | + this.charm. |
1063 | + */ |
1064 | + promiseCharm: function(charmId, cache, defaultSeries) { |
1065 | + var self = this; |
1066 | + return Y.Promise(function(resolve, reject) { |
1067 | + self.charm(charmId, { 'success': resolve, 'failure': reject }, |
1068 | + self, cache, defaultSeries); |
1069 | + }); |
1070 | + }, |
1071 | + |
1072 | + /** |
1073 | + Promises to return the latest charm ID for a given charm if a newer one |
1074 | + exists; this also caches the newer charm if one is available. |
1075 | + |
1076 | + @method promiseUpgradeAvailability |
1077 | + @param {Charm} charm An existing charm potentially in need of an upgrade. |
1078 | + @param {ModelList} cache A local cache of browser charms. |
1079 | + @return {Promise} A promise for a newer charm ID or undefined. |
1080 | + */ |
1081 | + promiseUpgradeAvailability: function(charm, cache) { |
1082 | + // Get the charm's store ID, then remove the version number to retrieve |
1083 | + // the latest version of the charm. |
1084 | + var storeId, revision; |
1085 | + if (charm instanceof Y.Model) { |
1086 | + storeId = charm.get('storeId'); |
1087 | + revision = parseInt(charm.get('revision'), 10); |
1088 | + } else { |
1089 | + storeId = charm.url; |
1090 | + revision = parseInt(charm.revision, 10); |
1091 | + } |
1092 | + storeId = storeId.replace(/-\d+$/, ''); |
1093 | + // XXX By using a cache we hide charm versions that have become available |
1094 | + // since we last requested the most recent version. |
1095 | + return this.promiseCharm(storeId, cache) |
1096 | + .then(function(latest) { |
1097 | + var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10); |
1098 | + if (latestVersion > revision) { |
1099 | + return latest.charm.id; |
1100 | + } |
1101 | + }, function(e) { |
1102 | + throw e; |
1103 | + }); |
1104 | + }, |
1105 | + |
1106 | + /** |
1107 | + * API call to search charms |
1108 | + * |
1109 | + * @method search |
1110 | + * @param {Object} filters the filters data object for search. |
1111 | + * @param {Object} callbacks the success/failure callbacks to use. |
1112 | + * @param {Object} bindScope the scope of *this* in the callbacks. |
1113 | + */ |
1114 | + search: function(filters, callbacks, bindScope) { |
1115 | + var endpoint = 'search'; |
1116 | + if (bindScope) { |
1117 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1118 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1119 | + } |
1120 | + this._makeRequest(endpoint, callbacks, filters); |
1121 | + }, |
1122 | + |
1123 | + /** |
1124 | + * Fetch the contents of a charm's file. |
1125 | + * |
1126 | + * @method file |
1127 | + * @param {String} charmID The id of the charm's file we want. |
1128 | + * @param {String} filename The path/name of the file to fetch content. |
1129 | + * @param {Object} callbacks The success/failure callbacks. |
1130 | + * @param {Object} bindScope The scope for this in the callbacks. |
1131 | + * |
1132 | + */ |
1133 | + file: function(charmID, filename, callbacks, bindScope) { |
1134 | + // If we're in the noop state, just call the error callback. |
1135 | + if (this.get('noop')) { |
1136 | + callbacks.failure('noop failure'); |
1137 | + return; |
1138 | + } |
1139 | + |
1140 | + var endpoint = 'charm/' + charmID + '/file/' + filename; |
1141 | + if (bindScope) { |
1142 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1143 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1144 | + } |
1145 | + |
1146 | + this.get('datasource').sendRequest({ |
1147 | + request: endpoint, |
1148 | + callback: { |
1149 | + 'success': function(io_request) { |
1150 | + callbacks.success(io_request.response.results[0].responseText); |
1151 | + }, |
1152 | + 'failure': function(io_request) { |
1153 | + var respText = io_request.response.results[0].responseText, |
1154 | + res; |
1155 | + if (respText) { |
1156 | + res = Y.JSON.parse(respText); |
1157 | + } |
1158 | + callbacks.failure(res, io_request); |
1159 | + } |
1160 | + } |
1161 | + }); |
1162 | + }, |
1163 | + |
1164 | + /** |
1165 | + Generate the API path to a charm icon. |
1166 | + This is useful when generating links and references in HTML to the |
1167 | + charm's icon and is constructing the correct icon based on reviewed |
1168 | + status and categories on the charm. |
1169 | + |
1170 | + @method iconpath |
1171 | + @param {String} charmID The id of the charm to grab the icon for. |
1172 | + @return {String} The URL of the charm's icon. |
1173 | + */ |
1174 | + iconpath: function(charmID) { |
1175 | + // If this is a local charm, then we need use a hard coded path to the |
1176 | + // default icon since we cannot fetch its category data or its own |
1177 | + // icon. |
1178 | + // XXX: #1202703 - this is a short term fix for the bug. Need longer |
1179 | + // term solution. |
1180 | + if (charmID.indexOf('local:') === 0) { |
1181 | + return this.get('apiHost') + |
1182 | + 'static/img/charm_160.svg'; |
1183 | + |
1184 | + } else { |
1185 | + // Get the charm ID from the service. In some cases, this will be |
1186 | + // the charm URL with a protocol, which will need to be removed. |
1187 | + // The following regular expression removes everything up to the |
1188 | + // colon portion of the quote and leaves behind a charm ID. |
1189 | + charmID = charmID.replace(/^[^:]+:/, ''); |
1190 | + return this.get('apiHost') + [ |
1191 | + this._apiRoot, |
1192 | + 'charm', |
1193 | + charmID, |
1194 | + 'file', |
1195 | + 'icon.svg'].join('/'); |
1196 | + } |
1197 | + }, |
1198 | + |
1199 | + /** |
1200 | + * Generate the url to an icon for the category specified. |
1201 | + * |
1202 | + * @method buildCategoryIconPath |
1203 | + * @param {String} categoryID the id of the category to load an icon for. |
1204 | + * |
1205 | + */ |
1206 | + buildCategoryIconPath: function(categoryID) { |
1207 | + return [ |
1208 | + this.get('apiHost'), |
1209 | + 'static/img/category-', |
1210 | + categoryID, |
1211 | + '-bw.svg' |
1212 | + ].join(''); |
1213 | + }, |
1214 | + |
1215 | + /** |
1216 | + * Load the QA data for a specific charm. |
1217 | + * |
1218 | + * @method qa |
1219 | + * @param {String} charmID The charm to fetch QA data for. |
1220 | + * @param {Object} callbacks The success/failure callbacks to use. |
1221 | + * @param {Object} bindScope The scope for 'this' in the callbacks. |
1222 | + * |
1223 | + */ |
1224 | + qa: function(charmID, callbacks, bindScope) { |
1225 | + var endpoint = 'charm/' + charmID + '/qa'; |
1226 | + if (bindScope) { |
1227 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1228 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1229 | + } |
1230 | + this._makeRequest(endpoint, callbacks); |
1231 | + }, |
1232 | + |
1233 | + /** |
1234 | + * Given a result list, append metadata is appended to the charm or bundle |
1235 | + * as the 'metadata' attribute and convert to objects. |
1236 | + * |
1237 | + * @method transformResults |
1238 | + * @param {Object} JSON decoded data from response. |
1239 | + * @return {Array} List of charm and bundle objects. |
1240 | + * |
1241 | + */ |
1242 | + transformResults: function(data) { |
1243 | + // Append the metadata to the actual token object. |
1244 | + var token; |
1245 | + var tokens; |
1246 | + tokens = Y.Array.map(data, function(entity) { |
1247 | + if (Y.Lang.isValue(entity.charm)) { |
1248 | + token = new Y.juju.models.Charm(entity.charm); |
1249 | + } else { |
1250 | + token = new Y.juju.models.Bundle(entity.bundle); |
1251 | + } |
1252 | + if (entity.metadata) { |
1253 | + token.set('metadata', entity.metadata); |
1254 | + } |
1255 | + return token; |
1256 | + }); |
1257 | + return tokens; |
1258 | + }, |
1259 | + |
1260 | + /** |
1261 | + * Initialize the API helper. Constructs a reusable datasource for all |
1262 | + * calls. |
1263 | + * |
1264 | + * @method initializer |
1265 | + * @param {Object} cfg configuration object. |
1266 | + * |
1267 | + */ |
1268 | + initializer: function(cfg) { |
1269 | + // XXX This isn't set on initial load so we have to manually hit the |
1270 | + // setter to get datasource filled in. Must be a better way. |
1271 | + this.set('apiHost', cfg.apiHost); |
1272 | + }, |
1273 | + |
1274 | + /** |
1275 | + * Fetch the interesting landing content from the charmworld API. |
1276 | + * |
1277 | + * @method interesting |
1278 | + * @return {Object} data loaded from the API call. |
1279 | + * |
1280 | + */ |
1281 | + interesting: function(callbacks, bindScope) { |
1282 | + if (bindScope) { |
1283 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1284 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1285 | + } |
1286 | + |
1287 | + this._makeRequest('search/interesting', callbacks); |
1288 | + }, |
1289 | + |
1290 | + /** |
1291 | + Fetch the related charm info from the charmworld API. |
1292 | + |
1293 | + @method related |
1294 | + @param {String} charmID The charm to find related charms for. |
1295 | + @param {Object} callbacks The success/failure callbacks to use. |
1296 | + @param {Object} bindscope An object scope to perform callbacks in. |
1297 | + @return {Object} data loaded from the API call. |
1298 | + |
1299 | + */ |
1300 | + related: function(charmID, callbacks, bindScope) { |
1301 | + var endpoint = 'charm/' + charmID + '/related'; |
1302 | + if (bindScope) { |
1303 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1304 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1305 | + } |
1306 | + this._makeRequest(endpoint, callbacks); |
1307 | + } |
1308 | + }, { |
1309 | + ATTRS: { |
1310 | + /** |
1311 | + * Required attribute for the host to talk to for API calls. |
1312 | + * |
1313 | + * @attribute apiHost |
1314 | + * @default undefined |
1315 | + * @type {String} |
1316 | + * |
1317 | + */ |
1318 | + apiHost: { |
1319 | + required: true, |
1320 | + 'setter': function(val) { |
1321 | + if (val && !val.match(/\/$/)) { |
1322 | + val = val + '/'; |
1323 | + } |
1324 | + // Make sure we update the datasource if our apiHost changes. |
1325 | + var source = val + this._apiRoot + '/'; |
1326 | + this.set('datasource', new Y.DataSource.IO({ source: source })); |
1327 | + return val; |
1328 | + } |
1329 | + }, |
1330 | + |
1331 | + /** |
1332 | + * Auto constructed datasource object based on the apiHost attribute. |
1333 | + * @attribute datasource |
1334 | + * @type {Datasource} |
1335 | + * |
1336 | + */ |
1337 | + datasource: { |
1338 | + 'setter': function(datasource) { |
1339 | + // Construct an API helper using the new datasource. |
1340 | + this.apiHelper = new ns.ApiHelper({ |
1341 | + sendRequest: Y.bind(datasource.sendRequest, datasource) |
1342 | + }); |
1343 | + return datasource; |
1344 | + } |
1345 | + }, |
1346 | + |
1347 | + /** |
1348 | + If there's no config we end up setting noop on the store so that tests |
1349 | + that don't need to worry about the browser can safely ignore it. |
1350 | + |
1351 | + We do log a console error, so those will occur on these tests to help |
1352 | + make it easy to catch an issue when you don't mean to noop the store. |
1353 | + |
1354 | + @attribute noop |
1355 | + @default false |
1356 | + @type {Boolean} |
1357 | + |
1358 | + */ |
1359 | + noop: { |
1360 | + value: false |
1361 | + } |
1362 | + } |
1363 | + }); |
1364 | + |
1365 | + /** |
1366 | + * Charmworld API version 2 interface. |
1367 | + * |
1368 | + * @class APIv2 |
1369 | + * @extends {APIv3} |
1370 | + * |
1371 | + * This class inherits from the v3 version of the API so that removing v2 |
1372 | + * once it is no longer needed will be easy (just delete this class, the one |
1373 | + * or two places it is referenced in the code, and its associated tests). |
1374 | + * |
1375 | + */ |
1376 | + ns.APIv2 = Y.Base.create('APIv2', ns.APIv3, [], { |
1377 | + _apiRoot: 'api/2', |
1378 | + |
1379 | + /** |
1380 | + * API call to fetch autocomplete suggestions based on the current term. |
1381 | + * |
1382 | + * @method autocomplete |
1383 | + * @param {Object} query the filters data object for search. |
1384 | + * @param {Object} filters the filters data object for search. |
1385 | + * @param {Object} callbacks the success/failure callbacks to use. |
1386 | + * @param {Object} bindScope the scope of *this* in the callbacks. |
1387 | + */ |
1388 | + autocomplete: function(filters, callbacks, bindScope) { |
1389 | + var endpoint = 'charms'; |
1390 | + // Force that this is an autocomplete call to perform matching on the |
1391 | + // start of names vs a fulltext search. |
1392 | + filters.autocomplete = 'true'; |
1393 | + filters.limit = 5; |
1394 | + if (bindScope) { |
1395 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1396 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1397 | + } |
1398 | + this._makeRequest(endpoint, callbacks, filters); |
1399 | + }, |
1400 | + |
1401 | + /** |
1402 | + * API call to fetch a charm's details, with an optional local cache. |
1403 | + * |
1404 | + * @method charmWithCache |
1405 | + * @param {String} charmID The charm to fetch This is the fully qualified |
1406 | + * charm name in the format scheme:series/charm-revision. |
1407 | + * @param {Object} callbacks The success/failure callbacks to use. |
1408 | + * @param {Object} bindScope The scope of "this" in the callbacks. |
1409 | + * @param {ModelList} [cache] a local cache of browser charms. |
1410 | + * @param {String} [defaultSeries='precise'] The series to use if none is |
1411 | + * specified in the charm ID. |
1412 | + */ |
1413 | + charm: function(charmID, callbacks, bindScope, cache, defaultSeries) { |
1414 | + if (bindScope) { |
1415 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1416 | + } |
1417 | + if (cache) { |
1418 | + var charm = cache.getById(charmID); |
1419 | + if (charm) { |
1420 | + // If the charm was found in the cache, then we can declare success |
1421 | + // without ever making a request to charmworld. |
1422 | + Y.soon(function() { |
1423 | + // Since there wasn't really a request, there is no data, so we |
1424 | + // pass an empty object as the "data" parameter. |
1425 | + callbacks.success({}, charm); |
1426 | + }); |
1427 | + return; |
1428 | + } else { |
1429 | + var successCB = callbacks.success; |
1430 | + callbacks.success = function(data) { |
1431 | + var charm = new Y.juju.models.Charm(data.charm); |
1432 | + if (data.metadata) { |
1433 | + charm.set('metadata', data.metadata); |
1434 | + } |
1435 | + cache.add(charm); |
1436 | + successCB(data, charm); |
1437 | + }; |
1438 | + } |
1439 | + } |
1440 | + charmID = this.apiHelper.normalizeCharmId(charmID, defaultSeries); |
1441 | + // If the charm ID does not have a revision number (or "HEAD"), add one. |
1442 | + if (/\-(\d+|HEAD)/.exec(charmID) === null) { |
1443 | + // Add in a revision placeholder. Any value will do, v2 of the |
1444 | + // charmworld API ignores revision numbers. |
1445 | + charmID = charmID + '-1'; |
1446 | + } |
1447 | + this._charm(charmID, callbacks, bindScope); |
1448 | + }, |
1449 | + |
1450 | + /** |
1451 | + Promises to return the latest charm ID for a given charm if a newer one |
1452 | + exists; this also caches the newer charm if one is available. |
1453 | + |
1454 | + @method promiseUpgradeAvailability |
1455 | + @param {Charm} charm An existing charm potentially in need of an upgrade. |
1456 | + @param {ModelList} cache A local cache of browser charms. |
1457 | + @return {Promise} A promise for a newer charm ID or undefined. |
1458 | + */ |
1459 | + promiseUpgradeAvailability: function(charm, cache) { |
1460 | + // Get the charm's store ID, then replace the version number |
1461 | + // with '-HEAD' to retrieve the latest version of the charm. |
1462 | + var storeId, revision; |
1463 | + if (charm instanceof Y.Model) { |
1464 | + storeId = charm.get('storeId'); |
1465 | + revision = parseInt(charm.get('revision'), 10); |
1466 | + } else { |
1467 | + storeId = charm.url; |
1468 | + revision = parseInt(charm.revision, 10); |
1469 | + } |
1470 | + storeId = storeId.replace(/-\d+$/, '-HEAD'); |
1471 | + // XXX By using a cache we hide charm versions that have become available |
1472 | + // since we last requested the most recent version. |
1473 | + return this.promiseCharm(storeId, cache) |
1474 | + .then(function(latest) { |
1475 | + var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10); |
1476 | + if (latestVersion > revision) { |
1477 | + return latest.charm.id; |
1478 | + } |
1479 | + }, function(e) { |
1480 | + throw e; |
1481 | + }); |
1482 | + }, |
1483 | + |
1484 | + /** |
1485 | + * API call to search charms |
1486 | + * |
1487 | + * @method search |
1488 | + * @param {Object} filters the filters data object for search. |
1489 | + * @param {Object} callbacks the success/failure callbacks to use. |
1490 | + * @param {Object} bindScope the scope of *this* in the callbacks. |
1491 | + */ |
1492 | + search: function(filters, callbacks, bindScope) { |
1493 | + var endpoint = 'charms'; |
1494 | + if (bindScope) { |
1495 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1496 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1497 | + } |
1498 | + this._makeRequest(endpoint, callbacks, filters); |
1499 | + }, |
1500 | + |
1501 | + /** |
1502 | + Generate the API path to a charm icon. |
1503 | + This is useful when generating links and references in HTML to the |
1504 | + charm's icon and is constructing the correct icon based on reviewed |
1505 | + status and categories on the charm. |
1506 | + |
1507 | + @method iconpath |
1508 | + @param {String} charmID The id of the charm to grab the icon for. |
1509 | + @return {String} The URL of the charm's icon. |
1510 | + */ |
1511 | + iconpath: function(charmID) { |
1512 | + // If this is a local charm, then we need use a hard coded path to the |
1513 | + // default icon since we cannot fetch its category data or its own |
1514 | + // icon. |
1515 | + // XXX: #1202703 - this is a short term fix for the bug. Need longer |
1516 | + // term solution. |
1517 | + if (charmID.indexOf('local:') === 0) { |
1518 | + return this.get('apiHost') + |
1519 | + 'static/img/charm_160.svg'; |
1520 | + |
1521 | + } else { |
1522 | + // Get the charm ID from the service. In some cases, this will be |
1523 | + // the charm URL with a protocol, which will need to be removed. |
1524 | + // The following regular expression removes everything up to the |
1525 | + // colon portion of the quote and leaves behind a charm ID. |
1526 | + charmID = charmID.replace(/^[^:]+:/, ''); |
1527 | + return this.get('apiHost') + [ |
1528 | + this._apiRoot, |
1529 | + 'charm', |
1530 | + charmID, |
1531 | + 'icon.svg'].join('/'); |
1532 | + } |
1533 | + }, |
1534 | + |
1535 | + /** |
1536 | + * Given a result list, turn that into an array of charm objects for the |
1537 | + * application to use. Metadata is appended to the charm or bundle as the |
1538 | + * 'metadata' attribute. |
1539 | + * |
1540 | + * @method transformResults |
1541 | + * @param {Object} JSON decoded data from response. |
1542 | + * @return {Array} List of charm objects. |
1543 | + * |
1544 | + */ |
1545 | + transformResults: function(data) { |
1546 | + // Remove non-charms (bundles) from the data. |
1547 | + data = Y.Array.filter(data, function(charmData) { |
1548 | + return Y.Lang.isValue(charmData.charm); |
1549 | + }); |
1550 | + // Append the metadata to the actual charm object. |
1551 | + data = Y.Array.map(data, function(charmData) { |
1552 | + var charm = new Y.juju.models.Charm(charmData.charm); |
1553 | + if (charmData.metadata) { |
1554 | + charm.set('metadata', charmData.metadata); |
1555 | + } |
1556 | + return charm; |
1557 | + }); |
1558 | + return data; |
1559 | + }, |
1560 | + |
1561 | + /** |
1562 | + * Fetch the interesting landing content from the charmworld API. |
1563 | + * |
1564 | + * @method interesting |
1565 | + * @return {Object} data loaded from the API call. |
1566 | + * |
1567 | + */ |
1568 | + interesting: function(callbacks, bindScope) { |
1569 | + if (bindScope) { |
1570 | + callbacks.success = Y.bind(callbacks.success, bindScope); |
1571 | + callbacks.failure = Y.bind(callbacks.failure, bindScope); |
1572 | + } |
1573 | + |
1574 | + this._makeRequest('charms/interesting', callbacks); |
1575 | + } |
1576 | + }, { |
1577 | + ATTRS: {} |
1578 | + }); |
1579 | + |
1580 | +}, '0.1.0', { |
1581 | + requires: [ |
1582 | + 'datasource-io', |
1583 | + 'json-parse', |
1584 | + 'juju-charm-models', |
1585 | + 'juju-bundle-models', |
1586 | + 'promise', |
1587 | + 'querystring-stringify' |
1588 | + ] |
1589 | +}); |
1590 | |
1591 | === modified file 'app/subapps/browser/browser.js' |
1592 | --- app/subapps/browser/browser.js 2013-09-17 13:27:43 +0000 |
1593 | +++ app/subapps/browser/browser.js 2013-09-30 21:13:03 +0000 |
1594 | @@ -921,8 +921,7 @@ |
1595 | var idBits = req.path.replace(/^\//, '').replace(/\/$/, '').split('/'), |
1596 | id = null; |
1597 | |
1598 | - if ((idBits.length === 3 && idBits[0][0] === '~') || // new charms |
1599 | - (idBits.length === 2)) { // reviewed charms |
1600 | + if (idBits.length > 1) { |
1601 | id = this._stripViewMode(req.path); |
1602 | } |
1603 | if (!id) { |
1604 | |
1605 | === modified file 'app/subapps/browser/views/charm.js' |
1606 | --- app/subapps/browser/views/charm.js 2013-09-24 15:34:52 +0000 |
1607 | +++ app/subapps/browser/views/charm.js 2013-09-30 21:13:03 +0000 |
1608 | @@ -39,7 +39,8 @@ |
1609 | views.utils.apiFailingView |
1610 | ], { |
1611 | |
1612 | - template: views.Templates.browser_charm, |
1613 | + charmTemplate: views.Templates.browser_charm, |
1614 | + bundleTemplate: views.Templates.bundle, |
1615 | qatemplate: views.Templates.browser_qa, |
1616 | |
1617 | /** |
1618 | @@ -706,7 +707,7 @@ |
1619 | tplData.provides = false; |
1620 | } |
1621 | |
1622 | - var tpl = this.template(tplData); |
1623 | + var tpl = this.charmTemplate(tplData); |
1624 | var tplNode = container.setHTML(tpl); |
1625 | |
1626 | // Set the content then update the container so that it reload |
1627 | @@ -752,35 +753,68 @@ |
1628 | }, |
1629 | |
1630 | /** |
1631 | + Handles rendering the bundle view into the view's container and then |
1632 | + appending that container into the DOM. |
1633 | + |
1634 | + @method _renderBundleView |
1635 | + @param {Y.Model} bundle the bundle model. |
1636 | + */ |
1637 | + _renderBundleView: function(bundle) { |
1638 | + var template = this.bundleTemplate(bundle.getAttrs()); |
1639 | + var container = this.get('container').setHTML(template); |
1640 | + this.get('renderTo').setHTML(container); |
1641 | + }, |
1642 | + |
1643 | + /** |
1644 | Render out the view to the DOM. |
1645 | |
1646 | - The View might be given either a charmID, which means go fetch the |
1647 | - charm data, or a charm model instance, in which case the view has the |
1648 | + The View might be given either a charmID, or bundleID, which means go |
1649 | + fetch the data, or a charm model instance, in which case the view has the |
1650 | data it needs to render. |
1651 | |
1652 | @method render |
1653 | |
1654 | */ |
1655 | render: function() { |
1656 | - var isFullscreen = this.get('isFullscreen'); |
1657 | - this.showIndicator(this.get('renderTo')); |
1658 | + var isFullscreen = this.get('isFullscreen'), |
1659 | + renderTo = this.get('renderTo'), |
1660 | + charmID = this.get('charmID'), |
1661 | + charmworld = this.get('store'); |
1662 | + |
1663 | + this.showIndicator(renderTo); |
1664 | |
1665 | if (this.get('charm')) { |
1666 | this._renderCharmView(this.get('charm'), isFullscreen); |
1667 | - this.hideIndicator(this.get('renderTo')); |
1668 | + this.hideIndicator(renderTo); |
1669 | + } else if (this.get('bundle')) { |
1670 | + this._renderBundleView(this.get('bundle'), isFullscreen); |
1671 | + this.hideIndicator(renderTo); |
1672 | } else { |
1673 | - this.get('store').charm(this.get('charmID'), { |
1674 | - 'success': function(data) { |
1675 | - var charm = new models.Charm(data.charm); |
1676 | - if (data.metadata) { |
1677 | - charm.set('metadata', data.metadata); |
1678 | - } |
1679 | - this.set('charm', charm); |
1680 | - this._renderCharmView(this.get('charm'), isFullscreen); |
1681 | - this.hideIndicator(this.get('renderTo')); |
1682 | - }, |
1683 | - 'failure': this.apiFailure |
1684 | - }, this); |
1685 | + if (charmID.indexOf('bundle') === -1) { |
1686 | + charmworld.charm(this.get('charmID'), { |
1687 | + 'success': function(data) { |
1688 | + var charm = new models.Charm(data.charm); |
1689 | + if (data.metadata) { |
1690 | + charm.set('metadata', data.metadata); |
1691 | + } |
1692 | + this.set('charm', charm); |
1693 | + this._renderCharmView(this.get('charm'), isFullscreen); |
1694 | + this.hideIndicator(renderTo); |
1695 | + }, |
1696 | + 'failure': this.apiFailure |
1697 | + }, this); |
1698 | + } else { |
1699 | + charmworld.bundle(charmID, { |
1700 | + 'success': function(data) { |
1701 | + var bundle = new models.Bundle(data); |
1702 | + this.set('bundle', bundle); |
1703 | + this._renderBundleView(bundle); |
1704 | + this.hideIndicator(renderTo); |
1705 | + }, |
1706 | + 'failure': this.apiFailure |
1707 | + }, this); |
1708 | + } |
1709 | + |
1710 | } |
1711 | } |
1712 | }, { |
1713 | |
1714 | === modified file 'app/templates/bundle.handlebars' |
1715 | --- app/templates/bundle.handlebars 2013-09-26 03:50:03 +0000 |
1716 | +++ app/templates/bundle.handlebars 2013-09-30 21:13:03 +0000 |
1717 | @@ -4,7 +4,7 @@ |
1718 | <div class="header"> |
1719 | <a href="" class="back icon"></a> |
1720 | <div class="block-icon"> |
1721 | - <img src="{{charmIconPath storeId}}" alt="{{ name }} icon" class="icon"> |
1722 | + <img src="{{storeId}}" alt="{{ name }} icon" class="icon"> |
1723 | </div> |
1724 | <div class="details"> |
1725 | <h1>{{ name }}</h1> |
1726 | @@ -96,7 +96,7 @@ |
1727 | <span class="date"> |
1728 | {{prettyDate}} |
1729 | </span> |
1730 | - <strong>{{author.name}}</strong> |
1731 | + <strong>{{author.name}}</strong> |
1732 | <span class="respect-whitespace">{{message}}</span> - |
1733 | <a href="{{ revnoLink }}"> |
1734 | REVNO{{revno}} |
1735 | |
1736 | === modified file 'undocumented' |
1737 | --- undocumented 2013-09-30 17:47:40 +0000 |
1738 | +++ undocumented 2013-09-30 21:13:03 +0000 |
1739 | @@ -4,10 +4,6 @@ |
1740 | app/app.js:214 "callback" |
1741 | app/app.js:225 "callback" |
1742 | app/app.js:239 "callback" |
1743 | -app/store/charm.js:518 "setter" |
1744 | -app/store/charm.js:65 "success" |
1745 | -app/store/charm.js:500 "setter" |
1746 | -app/store/charm.js:329 "success" |
1747 | app/store/notifications.js:167 "level" |
1748 | app/store/notifications.js:50 "title" |
1749 | app/store/notifications.js:156 "title" |
Reviewers: mp+188456_ code.launchpad. net,
Message:
Please take a look.
Description:
WIP - Bundle details view
This is a WIP fist pass for the bundle details view. Because there is
no way to access this from the application you can access it by
using the following URL:
http:// localhost: 8888/bundle/ ~bac/wiki/ 3/wiki/ :flags: /charmworldv3/
https:/ /code.launchpad .net/~hatch/ juju-gui/ bundle- detail- view/+merge/ 188456
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/14153043/
Affected files (+857, -797 lines): debug.js charmworld. js browser/ browser. js browser/ views/charm. js bundle. handlebars
A [revision details]
M app/modules-
D app/store/charm.js
A app/store/
M app/subapps/
M app/subapps/
M app/templates/
M undocumented