~bac/juju-gui/1103207

« back to all changes in this revision

Viewing changes to app/app.js

  • Committer: Brad Crittenden
  • Date: 2013-01-28 12:43:53 UTC
  • mfrom: (339.2.12 juju-gui)
  • Revision ID: bac@canonical.com-20130128124353-a11otqfucb8v2dev
MergeĀ fromĀ trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
'use strict';
 
2
 
2
3
/**
3
 
 * Provides the main app class.
 
4
 * Provides the main app class, based on the YUI App framework.
4
5
 *
5
6
 * @module app
6
7
 */
23
24
   * @class App
24
25
   */
25
26
  var JujuGUI = Y.Base.create('juju-gui', Y.App, [], {
 
27
 
 
28
    /*
 
29
     * Views
 
30
     *
 
31
     * The views encapsulate the functionality blocks that output
 
32
     * the GUI pages. The "parent" attribute defines the hierarchy.
 
33
     *
 
34
     *  @attribute views
 
35
     */
26
36
    views: {
27
 
      environment: {
28
 
        type: 'juju.views.environment',
29
 
        preserve: true
30
 
      },
31
37
 
32
38
      login: {
33
39
        type: 'juju.views.login',
34
40
        preserve: false
35
41
      },
36
42
 
 
43
      environment: {
 
44
        type: 'juju.views.environment',
 
45
        preserve: true
 
46
      },
 
47
 
37
48
      service: {
38
49
        type: 'juju.views.service',
39
50
        preserve: false,
87
98
 
88
99
    },
89
100
 
90
 
    /*
 
101
    /**
91
102
     * Data driven behaviors
92
103
     *
93
 
     * This is a placeholder for real behaviors associated with DOM Node data-*
 
104
     * Placeholder for real behaviors associated with DOM Node data-*
94
105
     * attributes.
95
106
     *
96
107
     *  @attribute behaviors
97
108
     */
98
109
    behaviors: {
99
110
      timestamp: {
 
111
        /**
 
112
         * Wait for the DOM to be built before rendering timestamps.
 
113
         */
100
114
        callback: function() {
101
115
          var self = this;
102
116
          Y.later(6000, this, function(o) {
111
125
    },
112
126
 
113
127
    /**
114
 
     * This method activates the keyboard listeners.
 
128
     * Activate the keyboard listeners. Only called by the main index.html,
 
129
     * not by the tests' one.
115
130
     */
116
131
    activateHotkeys: function() {
117
132
      Y.one(window).on('keydown', function(ev) {
126
141
          key.push('shift');
127
142
        }
128
143
        if (key.length === 0 &&
129
 
            // If we have no modifier, check if this is a function or the esc
130
 
            // key. It it is not one of these keys, just do nothing.
 
144
            // If we have no modifier, check if this is a function or the ESC
 
145
            // key. If it is not one of these keys, do nothing.
131
146
            !(ev.keyCode >= 112 && ev.keyCode <= 123 || ev.keyCode === 27)) {
132
147
          return; //nothing to do
133
148
        }
158
173
      }, this);
159
174
 
160
175
      /**
161
 
       * It transforms a numeric keyCode value to its string version. Example:
 
176
       * Transform a numeric keyCode value to its string version. Example:
162
177
       * 16 returns 'shift'.
163
178
       * @param {number} keyCode The numeric value of a key.
164
179
       * @return {string} The string version of the given keyCode.
193
208
     * @method initializer
194
209
     */
195
210
    initializer: function() {
196
 
      // If this flag is true, start the application with the console activated
197
 
      if (this.get('consoleEnabled')) {
198
 
        consoleManager.native();
199
 
      } else {
200
 
        consoleManager.noop();
 
211
      // If this flag is true, start the application
 
212
      // with the console activated.
 
213
      var consoleEnabled = this.get('consoleEnabled');
 
214
 
 
215
      // Concession to testing, they need muck with console, we cannot as well.
 
216
      if (window.mochaPhantomJS === undefined) {
 
217
        if (consoleEnabled) {
 
218
          consoleManager.native();
 
219
        } else {
 
220
          consoleManager.noop();
 
221
        }
201
222
      }
202
223
      // Create a client side database to store state.
203
224
      this.db = new models.Database();
212
233
        environment_node.set('text', environment_name);
213
234
      }
214
235
      // Create an environment facade to interact with.
215
 
      // allow env as an attr/option to ease testing
 
236
      // Allow "env" as an attribute/option to ease testing.
216
237
      if (this.get('env')) {
217
238
        this.env = this.get('env');
218
239
      } else {
236
257
        env: this.env,
237
258
        notifications: this.db.notifications});
238
259
 
239
 
      // Event subscriptions
240
 
 
241
260
      this.on('*:navigateTo', function(e) {
242
 
        console.log('navigateTo', e);
243
261
        this.navigate(e.url);
244
262
      }, this);
245
263
 
249
267
      // When the provider type becomes available, display it.
250
268
      this.env.after('providerTypeChange', this.onProviderTypeChange);
251
269
 
252
 
      // Once the user logs in we need to redraw.
 
270
      // Once the user logs in, we need to redraw.
253
271
      this.env.after('login', this.onLogin, this);
254
272
 
255
273
      // Feed environment changes directly into the database.
256
274
      this.env.on('delta', this.db.on_delta, this.db);
257
275
 
258
 
      // Feed delta changes to the notifications system
 
276
      // Feed delta changes to the notifications system.
259
277
      this.env.on('delta', this.notifications.generate_notices,
260
278
          this.notifications);
261
279
 
267
285
        }
268
286
      }, this);
269
287
 
270
 
      // If the database updates redraw the view (distinct from model updates)
271
 
      // TODO - Bound views will automatically update this on individual models
 
288
      // If the database updates, redraw the view (distinct from model updates)
 
289
      // TODO: bound views will automatically update this on individual models.
272
290
      this.db.on('update', this.on_database_changed, this);
273
291
 
274
 
      this.on('navigate', function(e) {
275
 
        console.log('app navigate', e);
276
 
      });
277
 
 
278
292
      this.enableBehaviors();
279
293
 
280
294
      this.once('ready', function(e) {
281
295
        if (this.get('socket_url')) {
282
296
          // Connect to the environment.
283
 
          console.log('App: Connecting to environment');
284
297
          this.env.connect();
285
298
        }
286
 
 
287
 
        console.log(
288
 
            'App: Re-rendering current view', this.getPath(), 'info');
289
 
 
290
299
        if (this.get('activeView')) {
291
300
          this.get('activeView').render();
292
301
        } else {
294
303
        }
295
304
      }, this);
296
305
 
297
 
      // Create the CharmPanel instance once the app.js is initialized
 
306
      // Create the CharmPanel instance once the app is initialized.
298
307
      var popup = views.CharmPanel.getInstance({
299
308
        charm_store: this.charm_store,
300
309
        env: this.env,
327
336
      var self = this;
328
337
      var active = this.get('activeView');
329
338
 
330
 
      // Compare endpoints map against db to see if it needs to be changed.
331
 
      var updateNeeded = this.db.services.some(function(service) {
 
339
      // Compare endpoints map against db to see if services have been added.
 
340
      var servicesAdded = this.db.services.some(function(service) {
332
341
        return (self.serviceEndpoints[service.get('id')] === undefined);
333
342
      });
334
343
 
335
344
      // If there are new services in the DB, pull an updated endpoints map.
336
 
      if (updateNeeded) {
 
345
      if (servicesAdded) {
337
346
        this.updateEndpoints();
338
347
      } else {
339
 
        // Check to see if any services have been removed (if there are, and
340
 
        // new ones also, updateEndpoints will replace the whole map, so only
341
 
        // do this if needed).
 
348
        // If any services have been removed, delete them from the map
 
349
        // rather than updating it as a whole.
342
350
        Y.Object.each(this.serviceEndpoints, function(key, value, obj) {
343
351
          if (self.db.services.getById(key) === null) {
344
352
            delete(self.serviceEndpoints[key]);
349
357
      // Redispatch to current view to update.
350
358
      if (active && active.name === 'EnvironmentView') {
351
359
        active.update();
 
360
        active.rendered();
352
361
      } else {
353
362
        this.dispatch();
354
363
      }
355
364
    },
356
365
 
357
366
    /**
358
 
     * When services are added we update endpoints here.
 
367
     * When services are added, we update endpoints here.
359
368
     *
360
369
     * @method updateEndpoints
361
370
     */
381
390
     * @method show_unit
382
391
     */
383
392
    show_unit: function(req) {
384
 
      console.log(
385
 
          'App: Route: Unit', req.params.id, req.path, req.pendingRoutes);
386
393
      // This replacement honors service names that have a hyphen in them.
387
394
      var unit_id = req.params.id.replace(/^(\S+)-(\d+)$/, '$1/$2');
388
395
      var unit = this.db.units.getById(unit_id);
406
413
     * @private
407
414
     */
408
415
    _prefetch_service: function(service) {
409
 
      // only prefetch once
410
 
      // we redispatch to the service view after we have status
 
416
      // Only prefetch once. We redispatch to the service view
 
417
      // after we have status.
411
418
      if (!service || service.get('prefetch')) { return; }
412
419
      service.set('prefetch', true);
413
420
 
446
453
        querystring: req.query
447
454
      }, {}, function(view) {
448
455
        // If the view contains a method call fitToWindow,
449
 
        // we will execute it after getting the view rendered
 
456
        // we will execute it after getting the view rendered.
450
457
        if (view.fitToWindow) {
451
458
          view.fitToWindow();
452
459
        }
517
524
    /**
518
525
     * Persistent Views
519
526
     *
520
 
     * 'notifications' is a preserved views that remains rendered on all main
521
 
     * views.  we manually create an instance of this view and insert it into
 
527
     * 'notifications' is a preserved view that remains rendered on all main
 
528
     * views.  We manually create an instance of this view and insert it into
522
529
     * the App's view metadata.
523
530
     *
524
531
     * @method show_notifications_view
619
626
     * Display the provider type.
620
627
     *
621
628
     * The provider type arrives asynchronously.  Instead of updating the
622
 
     * display from the environment code (a separation of concerns violation)
 
629
     * display from the environment code (a separation of concerns violation),
623
630
     * we update it here.
624
631
     *
625
632
     * @method onProviderTypeChange
637
644
          view = this.getViewInfo('environment'),
638
645
          options = {
639
646
            getModelURL: Y.bind(this.getModelURL, this),
640
 
            /** A simple closure so changes to the value are available.*/
 
647
            /** A simple closure so changes to the value are available. */
641
648
            getServiceEndpoints: function() {
642
649
              return self.serviceEndpoints;},
643
650
            loadService: this.loadService,
645
652
            env: this.env};
646
653
 
647
654
      this.showView('environment', options, {
 
655
        /**
 
656
         * Let the component framework know that the view has been rendered.
 
657
         */
648
658
        callback: function() {
649
659
          this.views.environment.instance.rendered();
650
660
        },
657
667
     * @method load_service
658
668
     */
659
669
    loadService: function(evt) {
660
 
      console.log('load service', evt);
661
670
      if (evt.err) {
662
671
        this.db.notifications.add(
663
672
            new models.Notification({
687
696
    /**
688
697
     * Object routing support
689
698
     *
690
 
     * This is a utility that helps map from model objects to routes
 
699
     * This utility helps map from model objects to routes
691
700
     * defined on the App object.
692
701
     *
693
702
     * To support this we supplement our routing information with
695
704
     *
696
705
     * model: model.name (required)
697
706
     * reverse_map: (optional) A reverse mapping of route_path_key to the
698
 
     *   name of the attribute on the model.  If no value is provided its
 
707
     *   name of the attribute on the model.  If no value is provided, it is
699
708
     *   used directly as attribute name.
700
709
     * intent: (optional) A string named intent for which this route should
701
710
     *   be used. This can be used to select which subview is selected to
702
 
     *   resolve a models route.
 
711
     *   resolve a model's route.
703
712
     *
704
713
     * @method getModelURL
705
714
     * @param {object} model The model to determine a route url for.
706
715
     * @param {object} [intent] the name of an intent associated with a route.
707
 
     *   When more than one route can match a model the route w/o an intent is
708
 
     *   matched when this attribute is missing.  If intent is provided as a
709
 
     *   string it is matched to the 'intent' attribute specified on the route.
710
 
     *   This is effectively a tag.
 
716
     *   When more than one route can match a model, the route without an
 
717
     *   intent is matched when this attribute is missing.  If intent is
 
718
     *   provided as a string, it is matched to the 'intent' attribute
 
719
     *   specified on the route. This is effectively a tag.
711
720
     *
 
721
     * @method getModelURL
712
722
     */
713
723
    getModelURL: function(model, intent) {
714
724
      var matches = [],
722
732
            required_model = route.model,
723
733
            reverse_map = route.reverse_map;
724
734
 
725
 
        // Fail fast on wildcard paths, routes w/o models
726
 
        // and when the model doesn't match the route type
 
735
        // Fail fast on wildcard paths, on routes without models,
 
736
        // and when the model does not match the route type.
727
737
        if (path === '*' ||
728
738
            required_model === undefined ||
729
739
            model.name !== required_model) {
730
740
          return;
731
741
        }
732
742
 
733
 
        // Replace the path params with items from the
734
 
        // models attrs
 
743
        // Replace the path params with items from the model's attributes.
735
744
        path = path.replace(regexPathParam,
736
745
            function(match, operator, key) {
737
746
              if (reverse_map !== undefined && reverse_map[key]) {
745
754
          intent: route.intent}));
746
755
      });
747
756
 
748
 
      // See if intent is in the match. Because the default is
749
 
      // to match routes without intent (undefined) this test
750
 
      // can always be applied.
 
757
      // See if intent is in the match. Because the default is to match routes
 
758
      // without intent (undefined), this test can always be applied.
751
759
      matches = Y.Array.filter(matches, function(match) {
752
760
        return match.intent === intent;
753
761
      });
754
762
 
755
763
      if (matches.length > 1) {
756
764
        console.warn('Ambiguous routeModel', attrs.id, matches);
757
 
        // Default to the last route in this configuration
758
 
        // error case.
 
765
        // Default to the last route in this configuration error case.
759
766
        idx = matches.length - 1;
760
767
      }
761
768
      return matches[idx] && matches[idx].path;
771
778
    _setRoutes: function(routes) {
772
779
      this._routes = [];
773
780
      Y.Array.each(routes, function(route) {
774
 
        // additionally pass route as options
775
 
        // needed to pass through the attribute setter
 
781
        // Additionally pass route as options. This is needed to pass through
 
782
        // the attribute setter.
776
783
        this.route(route.path, route.callback, route);
777
784
      }, this);
778
785
      return this._routes.concat();
784
791
    route: function(path, callback, options) {
785
792
      JujuGUI.superclass.route.call(this, path, callback);
786
793
 
787
 
      // This is a whitelist to spare the extra juggling
 
794
      // Use a whitelist to spare the extra juggling.
788
795
      if (options.model) {
789
796
        var r = this._routes,
790
797
                idx = r.length - 1;
791
798
        if (r[idx].path === path) {
792
 
          // Combine our options with the default
793
 
          // computed route information
 
799
          // Combine our options with the default computed route information.
794
800
          r[idx] = Y.mix(r[idx], options);
795
801
        } else {
796
802
          console.error(
803
809
  }, {
804
810
    ATTRS: {
805
811
      charm_store: {},
 
812
      /*
 
813
       * Routes
 
814
       *
 
815
       * Each request path is evaluated against all hereby defined routes,
 
816
       * and the callbacks for all the ones that match are invoked,
 
817
       * without stopping at the first one.
 
818
       *
 
819
       *  @attribute routes
 
820
       */
806
821
      routes: {
807
822
        value: [
 
823
          // Called on each request.
808
824
          { path: '*', callback: 'check_user_credentials'},
809
825
          { path: '*', callback: 'show_notifications_view'},
 
826
          // Charms.
810
827
          { path: '/charms/', callback: 'show_charm_collection'},
811
828
          { path: '/charms/*charm_store_path',
812
829
            callback: 'show_charm',
813
830
            model: 'charm'},
 
831
          // Notifications.
814
832
          { path: '/notifications/',
815
833
            callback: 'show_notifications_overview'},
 
834
          // Services.
816
835
          { path: '/service/:id/config',
817
836
            callback: 'show_service_config',
818
837
            intent: 'config',
828
847
          { path: '/service/:id/',
829
848
            callback: 'show_service',
830
849
            model: 'service'},
 
850
          // Units.
831
851
          { path: '/unit/:id/',
832
852
            callback: 'show_unit',
833
853
            reverse_map: {id: 'urlName'},
834
854
            model: 'serviceUnit'},
 
855
          // Root.
835
856
          { path: '/', callback: 'show_environment'}
836
857
        ]
837
858
      }
847
868
    'juju-charm-store',
848
869
    'juju-models',
849
870
    'juju-notifications',
850
 
    // This alias doesn't seem to work, including refs by hand.
 
871
    // This alias does not seem to work, including references by hand.
851
872
    'juju-controllers',
852
873
    'juju-notification-controller',
853
874
    'juju-env',