Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / ui / web / ui_vms_base_view.js @ d42817ee

History | View | Annotate | Download (26.3 kB)

1
// Copyright 2011 GRNET S.A. All rights reserved.
2
// 
3
// Redistribution and use in source and binary forms, with or
4
// without modification, are permitted provided that the following
5
// conditions are met:
6
// 
7
//   1. Redistributions of source code must retain the above
8
//      copyright notice, this list of conditions and the following
9
//      disclaimer.
10
// 
11
//   2. Redistributions in binary form must reproduce the above
12
//      copyright notice, this list of conditions and the following
13
//      disclaimer in the documentation and/or other materials
14
//      provided with the distribution.
15
// 
16
// THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
// USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
// POSSIBILITY OF SUCH DAMAGE.
28
// 
29
// The views and conclusions contained in the software and
30
// documentation are those of the authors and should not be
31
// interpreted as representing official policies, either expressed
32
// or implied, of GRNET S.A.
33
// 
34

    
35
;(function(root){
36
    
37
    // root
38
    var root = root;
39
    
40
    // setup namepsaces
41
    var snf = root.synnefo = root.synnefo || {};
42
    var models = snf.models = snf.models || {}
43
    var storage = snf.storage = snf.storage || {};
44
    var ui = snf.ui = snf.ui || {};
45

    
46
    var views = snf.views = snf.views || {}
47

    
48
    // shortcuts
49
    var bb = root.Backbone;
50
    
51
    // logging
52
    var logger = new snf.logging.logger("SNF-VIEWS");
53
    var debug = _.bind(logger.debug, logger);
54
    
55
    var hasKey = Object.prototype.hasOwnProperty;
56

    
57
    // base class for views that contain/handle VMS
58
    views.VMListView = views.View.extend({
59

    
60
        // just a flag to identify that
61
        // views of this type handle vms
62
        vms_view: true,
63

    
64
        selectors: {},
65
        hide_actions: true,
66
        pane: "#machines-pane",
67
        metadata_view: undefined,
68

    
69
        initialize: function() {
70
            views.VMListView.__super__.initialize.call(this);
71
            this._vm_els = {};
72
            this.set_storage_handlers();
73
            this.set_handlers();
74
            this.vms_updated_handler();
75
            this.connect_overlay = new views.VMConnectView();
76
        },
77

    
78
        // display vm diagnostics detail overlay view, update based on
79
        // diagnostics_update_interval config value.
80
        show_build_details_for_vm: function(vm) {
81
            var cont = null;
82
            var success = function(data) {
83
                var message = "";
84
                var title = vm.get('name');
85
                var info = '<em>Status log messages:</em>';
86

    
87
                var list_el = $('<div class="diagnostics-list">');
88
                list_el.append('<div class="empty">No data available</div>');
89

    
90
                cont = list_el;
91
                var messages = _.clone(data);
92

    
93
                update_overlay_diagnostics(data, cont);
94
                synnefo.ui.main.details_view.show(title, info, cont);
95
            }
96

    
97
            var update_overlay_diagnostics = function(data) {
98
                var existing = cont.find(".msg-log-entry");
99
                var messages = _.clone(data);
100
                
101
                var to_append = messages.slice(0, messages.length - existing.length);
102
                to_append.reverse();
103

    
104
                var appending = to_append.length != messages.length;
105
                
106
                if (to_append.length) { cont.find(".empty").hide() } else {
107
                    if (!messages.length) { cont.find(".empty").show() }
108
                }
109
                _.each(to_append, function(msg){
110
                    var el = $('<div class="clearfix msg-log-entry ' + msg.level.toLowerCase() + '">');
111
                    if (msg.details) {
112
                      el.addClass("with-details");
113
                    }
114
                    var display_source_date = synnefo.config.diagnostics_display_source_date;
115
                    var source_date = "";
116
                    if (display_source_date) {
117
                        source_date = '('+synnefo.util.formatDate(new Date(msg.source_date))+')';
118
                    }
119

    
120
                    el.append('<span class="date">' + synnefo.util.formatDate(new Date(msg.created)) + source_date + '</span>');
121
                    el.append('<span class="src">' + _.escape(msg.source) + '</span>');
122
                    el.append('<span class="msg">' + _.escape(msg.message) + '</span>');
123
                    if (msg.details) {
124
                        el.append('<pre class="details">' + _.escape(msg.details) + '</pre>');
125
                    }
126
                    if (appending) { el.hide(0); el.css({'display':'none'})}
127
                    cont.prepend(el);
128
                    el.click(function(el) {
129
                        $(this).find(".details").slideToggle();
130
                        $(this).toggleClass("expanded");
131
                    });
132

    
133
                    if (appending) { el.fadeIn(800); }
134
                });
135
                
136
                window.setTimeout(function(){
137
                    if (cont.is(":visible")) {
138
                        vm.get_diagnostics(update_overlay_diagnostics);
139
                    }
140
                }, synnefo.config.diagnostics_update_interval);
141
            }
142

    
143
            vm.get_diagnostics(success);
144
        },
145

    
146
        // Helpers
147
        //
148
        // get element based on this.selectors key/value pairs
149
        sel: function(id, params) {
150
            if (!this.selectors[id]){ return };
151
            return $(this.selectors[id].format(params));
152
        },
153
        
154
        // vm element based on vm model instance provided
155
        vm: function(vm) {
156
            if (hasKey.call(this._vm_els, vm.id)) {
157
                ret = this._vm_els[vm.id];
158
            } else {
159
                return $([]);
160
            }
161

    
162
            return ret;
163
        },
164
        
165
        // get vm model instance from DOM element
166
        vm_for_element: function(el) {
167
            return storage.vms.sel(this.vm_id_for_element(el));
168
        },
169
        
170

    
171
        // Event binding and stuff like that
172
        //
173
        set_storage_handlers: function() {
174
            storage.vms.bind("add", _.bind(this.vms_updated_handler, this, "add"));
175
            storage.vms.bind("change", _.bind(this.vms_updated_handler, this, "change"));
176
            storage.vms.bind("reset", _.bind(this.vms_updated_handler, this, "reset"));
177
            storage.vms.bind("remove", _.bind(this.vms_updated_handler, this, "remove"));
178
        },
179
        
180
        // vms updated triggered, update view vms
181
        vms_updated_handler: function (method, model, arg2, arg3) {
182
            var updated = storage.vms.models;
183
            if (method == "add") { updated = [model] };
184
            if (method == "change") { updated = [model] };
185
            if (method == "remove") { updated = [model] };
186

    
187
            if (method == "remove") {
188
                this.remove_vm(model)
189
                return;
190
            }
191
            
192
            this.update_vms(updated);
193
        },
194

    
195
        // create vm
196
        // append it on proper view container
197
        create_vm: function(vm) {
198
            // create dom element
199
            var vm_view = this.create_vm_element(vm);
200
            vm_view.find(".vm-actions").attr("id", this.view_id+"-actions-" + vm.id);
201
            this._vm_els[vm.id] = vm_view;
202
            var container = this.get_vm_container(vm);
203
            container.append(vm_view);
204
            vm_view.find(".action-indicator").text("");
205
            if (this.visible()) {
206
                container.show()
207
            }
208

    
209
            // initialize vm specific event handlers 
210
            this.__set_vm_handlers(vm);
211
            vm_view.find(".suspended-notice").click(function(){
212
              synnefo.ui.main.suspended_view.show(vm);
213
            })
214
            return vm_view;
215
        },
216
        
217
        // create vm dom element
218
        create_vm_element: function(vm) {
219
            // clone template
220
            return this.sel('tpl').clone().attr("id", this.id_tpl + vm.id)
221
        },
222

    
223
        // get proper vm container
224
        get_vm_container: function(vm) {
225
            if (vm.is_active()) {
226
                return this.sel("vm_cont_active");
227
            } else {
228
                return this.sel("vm_cont_terminated");
229
            }
230
        },
231

    
232
        // create and append inside the proper container the vm model
233
        // if it doesn't exist update vm data and make it visible
234
        add: function(vm) {
235
            // create if it does not exist
236
            if (!hasKey.call(this._vm_els, vm.id)) {
237
                var el = this.create_vm(vm);
238
                el.show();
239
                this.post_add(vm);
240
                this.init_vm_view_handlers(vm);
241
            }
242

    
243
            return this.vm(vm);
244
        },
245

    
246
        init_vm_view_handlers: function(vm) {
247
            var self = this;
248
            var el = this.vm(vm);
249

    
250
            // hidden feature, double click on indicators to display 
251
            // vm diagnostics.
252
            el.find(".indicators").bind("dblclick", function(){
253
                self.show_build_details_for_vm(vm);
254
            });
255

    
256
            // this button gets visible if vm creation failed.
257
            el.find("div.build-progress .btn").click(function(){
258
                self.show_build_details_for_vm(vm);
259
            });
260
        },
261
        
262
        // helpers for VMListView descendants
263
        post_add: function(vm) { throw "Not implemented" },
264
        set_vm_handlers: function(vm) { throw "Not implemented" },
265
        set_handlers: function() { throw "Not implemented" },
266
        update_layout: function() { throw "Not implemented" },
267
        post_update_vm: function(vm) { throw "Not implemented" },
268
        update_details: function(vm) {},
269
        
270
        // remove vm
271
        remove_vm: function(vm) {
272
            // FIXME: some kind of transiton ??? effect maybe ???
273
            this.vm(vm).remove();
274
            this.post_remove_vm(vm);
275
            if (hasKey.call(this._vm_els, vm.id)) {
276
                delete this._vm_els[vm.id];
277
            }
278
        },
279
        
280
        // remove all vms from view
281
        clear: function() {
282
            this.sel('vms').remove();
283
            this.__update_layout();
284
        },
285

    
286
        show: function() {
287
            views.VMListView.__super__.show.apply(this, arguments);
288
            if (storage.vms.length == 0) { this.hide() };
289
            if (!snf.config.update_hidden_views) {
290
                this.update_vms(storage.vms.models);
291
            }
292
        },
293

    
294
        // do update for provided vms, then update the view layout
295
        update_vms: function(vms) {
296
            if (!this.visible() && !snf.config.update_hidden_views) { return };
297

    
298
            _.each(vms, _.bind(function(vm){
299
                // vm will be removed
300
                // no need to update
301
                if (vm.get("status") == "DELETED") {
302
                    return;
303
                }
304

    
305
                // this won't add it additional times
306
                this.add(vm);
307
                this.update_vm(vm);
308
            }, this))
309
            
310
            // update view stuff
311
            this.__update_layout();
312
        },
313
        
314
        // update ui for the given vm
315
        update_vm: function(vm) {
316
            // do not update deleted state vms
317
            if (!vm || vm.get("status") == 'DELETED') { return };
318
            this.check_vm_container(vm);
319

    
320
            this.update_details(vm);
321
            this.update_transition_state(vm);
322

    
323
            if (this.action_views) {
324
                this.action_views[vm.id].update();
325
                this.action_views[vm.id].update_layout();
326
            }
327
            
328
            var el = this.vm(vm);
329
            if (vm.get('suspended')) {
330
              el.addClass("suspended");
331
            } else {
332
              el.removeClass("suspended");
333
            }
334

    
335
            try {
336
                this.post_update_vm(vm);
337
            } catch (err) {};
338
        },
339

    
340
        // check if vm is placed properly within the view
341
        // container (e.g. some views might have different
342
        // containers for terminated or running machines
343
        check_vm_container: function(vm){
344
            var el = this.vm(vm);
345
            if (!el.length) { return };
346
            var self = this;
347
            var selector = vm.is_active() ? 'vm_cont_active' : 'vm_cont_terminated';
348
            if (el.parent()[0] != this.sel(selector)[0]) {
349
                var cont = this.sel(selector);
350
                var self = this;
351

    
352
                el.hide().appendTo(cont).fadeIn(300);
353
                $(window).trigger('resize');
354

    
355
                //el.fadeOut(200, function() {
356
                    //el.appendTo(cont); 
357
                    //el.fadeIn(200);
358
                    //self.sel(selector).show(function(){
359
                        //$(window).trigger("resize");
360
                    //});
361
                //});
362
            }
363
        },
364

    
365
        __update_layout: function() {
366
            this.update_layout();
367
        },
368
        
369
        // append handlers for vm specific events
370
        __set_vm_handlers: function(vm) {
371
            // show transition on vm status transit
372
            vm.bind('transition', _.bind(function(){this.show_transition(vm)}, this));
373
            this.set_vm_handlers(vm);
374
        },
375
        
376
        // is vm in transition ??? show the progress spinner
377
        update_transition_state: function(vm) {
378
            if (vm.in_transition() && !vm.has_pending_action()){
379
                this.sel('vm_spinner', vm.id).show();
380
            } else {
381
                this.sel('vm_spinner', vm.id).hide();
382
            }
383
        },
384
        
385
        show_indicator: function(vm, action) {
386
            var action = action || vm.pending_action;
387
            this.sel('vm_wave', vm.id).hide();
388
            this.sel('vm_spinner', vm.id).hide();
389
            this.vm(vm).find(".action-indicator").removeClass().addClass(action + " action-indicator").show();
390
        },
391

    
392
        hide_indicator: function(vm) {
393
            this.vm(vm).find(".action-indicator").removeClass().addClass("action-indicator").hide();
394
            this.update_transition_state(vm);
395
        },
396

    
397
        // display transition animations
398
        show_transition: function(vm) {
399
            var wave = this.sel('vm_wave', vm.id);
400
            if (!wave || !wave.length) { return }
401

    
402
            var src = wave.attr('src');
403
            // change src to force gif play from the first frame
404
            // animate for 500 ms then hide
405
            wave.attr('src', "").show();
406
            wave.attr('src', src).fadeIn(200).delay(700).fadeOut(300, function() {
407
                wave.hide();
408
            });
409
        },
410

    
411
        connect_to_console: function(vm) {
412
            // It seems that Safari allows popup windows only if the window.open
413
            // call is made within an html element click event context. 
414
            // Otherwise its behaviour is based on "Block Pop-Up Windows" 
415
            // setting, which when enabled no notification appears for the user. 
416
            // Since there is no easy way to check for the setting value we use
417
            // async:false for the action call so that action success handler 
418
            // which opens the new window is called inside the click event 
419
            // context.
420
            var use_async = true;
421
            if ($.client.browser == "Safari") {
422
                use_async = false;
423
            }
424

    
425
            vm.call("console", function(console_data) {
426
                var url = vm.get_console_url(console_data);
427
                snf.util.open_window(url, "VM_" + vm.get("id") + "_CONSOLE", {'scrollbars': 1, 'fullscreen': 0});
428
            }, undefined, {async: use_async});
429
        }
430

    
431
    });
432
    
433
    // empty message view (just a wrapper to the element containing 
434
    // the empty information message)
435
    views.EmptyView = views.View.extend({
436
        el: '#emptymachineslist'
437
    })
438

    
439
    views.VMActionsView = views.View.extend({
440
        
441
        initialize: function(vm, parent, el, hide) {
442
            this.hide = hide || false;
443
            this.view = parent;
444
            this.vm = vm;
445
            this.vm_el = el;
446
            this.el = $("#" + parent.view_id + "-actions-" + vm.id);
447
            this.all_action_names = _.keys(views.VMActionsView.STATUS_ACTIONS);
448
            
449
            // state params
450
            this.selected_action = false;
451

    
452
            _.bindAll(this);
453
            window.acts = this;
454
            this.view_id = "vm_" + vm.id + "_actions";
455
            views.VMActionsView.__super__.initialize.call(this);
456

    
457
            this.hovered = false;
458
            this.set_hover_handlers();
459
        },
460

    
461
        action: function(name) {
462
            return $(this.el).find(".action-container." + name);
463
        },
464

    
465
        action_link: function(name) {
466
            return this.action(name).find("a");
467
        },
468
        
469
        action_confirm_cont: function(name) {
470
            return this.action_confirm(name).parent();
471
        },
472

    
473
        action_confirm: function(name) {
474
            return this.action(name).find("button.yes");
475
        },
476

    
477
        action_cancel: function(name) {
478
            return this.action(name).find("button.no");
479
        },
480

    
481
        hide_actions: function() {
482
            $(this.el).find("a").css("visibility", "hidden");
483
        },
484

    
485
        // update the actions layout, depending on the selected actions
486
        update_layout: function() {
487

    
488
            if (!this.vm_handlers_initialized) {
489
                this.vm = storage.vms.get(this.vm.id);
490
                this.init_vm_handlers();
491
            }
492

    
493
            if (!this.vm) { return }
494
            
495
            if (!this.hovered && !this.vm.has_pending_action() && this.hide && !this.vm.action_error) { 
496
                this.el.hide();
497
                this.view.hide_indicator(this.vm);
498
                return 
499
            };
500

    
501
            if (!this.el.is(":visible") && !this.hide) { return };
502
            if (!this.handlers_initialized) { this.set_handlers(); }
503

    
504

    
505
            // update selected action
506
            if (this.vm.pending_action) {
507
                this.selected_action = this.vm.pending_action;
508
            } else {
509
                this.selected_action = false;
510
            }
511
            
512
            // vm actions tha can be performed
513
            var actions = this.vm.get_available_actions();
514
            
515
            // had pending action but actions changed and now selected action is
516
            // not available, hide it from user
517
            if (this.selected_action && actions.indexOf(this.selected_action) == -1) {
518
                this.reset();
519
            }
520
            
521
            this.el.show();
522
            
523
            if ((this.selected_action || this.hovered) && !this.vm.action_error) {
524
                // show selected action
525
                $(this.el).show();
526
                $(this.el).find("a").css("visibility", "visible");
527
                // show action icon
528
                this.view.show_indicator(this.vm);
529
            } else {
530
                if (this.hide || this.vm.action_error) {
531
                    // view shows actions on machine hover
532
                    $(this.el).find("a").css("visibility", "hidden");
533
                } else {
534
                    if (!this.vm.action_error) {
535
                        // view shows actions always
536
                        $(this.el).find("a").css("visibility", "visible");
537
                        $(this.el).show();
538
                    }
539
                }
540
                
541
                this.view.hide_indicator(this.vm);
542
            }
543
                
544
            // update action link styles and shit
545
            _.each(models.VM.ACTIONS, function(action, index) {
546
                if (actions.indexOf(action) > -1) {
547
                    this.action(action).removeClass("disabled");
548
                    if (this.selected_action == action) {
549
                        this.action_confirm_cont(action).css('display', 'block');
550
                        this.action_confirm(action).show();
551
                        this.action(action).removeClass("disabled");
552
                        this.action_link(action).addClass("selected");
553
                    } else {
554
                        this.action_confirm_cont(action).hide();
555
                        this.action_confirm(action).hide();
556
                        this.action_link(action).removeClass("selected");
557
                    }
558
                } else {
559
                    this.action().hide();
560
                    this.action(action).addClass("disabled");
561
                    this.action_confirm(action).hide();
562
                }
563
            }, this);
564
        },
565
        
566
        init_vm_handlers: function() {
567
            try {
568
                this.vm.unbind("action:fail", this.update_layout)
569
                this.vm.unbind("action:fail:reset", this.update_layout)
570
            } catch (err) { console.log("Error")};
571
            
572
            this.vm.bind("action:fail", this.update_layout)
573
            this.vm.bind("action:fail:reset", this.update_layout)
574

    
575
            this.vm_handlers_initialized = true;
576
        },
577
        
578
        set_hover_handlers: function() {
579
            // vm container hover (icon view)
580
            this.view.vm(this.vm).hover(_.bind(function() {
581
                this.hovered = true;
582
                this.update_layout();
583

    
584
            }, this),  _.bind(function() {
585
                this.hovered = false;
586
                this.update_layout();
587
            }, this));
588
        },
589

    
590
        // bind event handlers
591
        set_handlers: function() {
592
            var self = this;
593
            var vm = this.vm;
594
            
595
            // initial hide
596
            if (this.hide) { $(this.el).hide() };
597
            
598
            // action links events
599
            _.each(models.VM.ACTIONS, function(action) {
600
                var action = action;
601
                // indicator hovers
602
                this.view.vm(this.vm).find(".action-container."+action+" a").hover(function() {
603
                    self.view.show_indicator(self.vm, action);
604
                }, function() {
605
                    // clear or show selected action indicator
606
                    if (self.vm.pending_action) {
607
                        self.view.show_indicator(self.vm);
608
                    } else {
609
                        self.view.hide_indicator(self.vm);
610
                    }
611
                })
612
                
613
                // action links click events
614
                $(this.el).find(".action-container."+action+" a").click(function(ev) {
615
                    ev.preventDefault();
616
                    self.set(action);
617
                }).data("action", action);
618

    
619
                // confirms
620
                $(this.el).find(".action-container."+action+" button.no").click(function(ev) {
621
                    ev.preventDefault();
622
                    self.reset();
623
                });
624

    
625
                // cancels
626
                $(this.el).find(".action-container."+action+" button.yes").click(function(ev) {
627
                    ev.preventDefault();
628
                    // override console
629
                    // ui needs to act (open the console window)
630
                    if (action == "console") {
631
                        self.view.connect_to_console(self.vm);
632
                    } else {
633
                        self.vm.call(action);
634
                    }
635
                    self.reset();
636
                });
637
            }, this);
638

    
639
            this.handlers_initialized = true;
640
        },
641
        
642
        // reset actions
643
        reset: function() {
644
            var prev_action = this.selected_action;
645
            this.selected_action = false;
646
            this.vm.clear_pending_action();
647
            this.trigger("change", {'action': prev_action, 'vm': this.vm, 'view': this, remove: true});
648
            this.trigger("remove", {'action': prev_action, 'vm': this.vm, 'view': this, remove: true});
649
        },
650
        
651
        // set selected action
652
        set: function(action_name) {
653
            this.selected_action = action_name;
654
            this.vm.update_pending_action(this.selected_action);
655
            this.view.vm(this.vm).find(".action-indicator").show().removeClass().addClass(action_name + " action-indicator");
656
            this.trigger("change", {'action': this.selected_action, 'vm': this.vm, 'view': this});
657
        },
658

    
659
        update: function() {
660
        }
661
    })
662

    
663

    
664
    views.VMActionsView.STATUS_ACTIONS = { 
665
        'reboot':        ['UNKOWN', 'ACTIVE', 'REBOOT'],
666
        'shutdown':      ['UNKOWN', 'ACTIVE', 'REBOOT'],
667
        'console':       ['ACTIVE'],
668
        'start':         ['UNKOWN', 'STOPPED'],
669
        'destroy':       ['UNKOWN', 'ACTIVE', 'STOPPED', 'REBOOT', 'ERROR', 'BUILD']
670
    };
671

    
672
    // UI helpers
673
    var uihelpers = snf.ui.helpers = {};
674
    
675
    // OS icon helpers
676
    var os_icon = uihelpers.os_icon = function(os) {
677
        var icons = window.os_icons;
678
        if (!icons) { return "unknown" }
679
        if (icons.indexOf(os) == -1) {
680
            os = "unknown";
681
        }
682
        return os;
683
    }
684

    
685
    var os_icon_path = uihelpers.os_icon_path = function(os, size, active) {
686
        size = size || "small";
687
        if (active == undefined) { active = true };
688

    
689
        var icon = os_icon(os);
690
        if (active) {
691
            icon = icon + "-on";
692
        } else {
693
            icon = icon + "-off";
694
        }
695

    
696
        return (snf.config.machines_icons_url + "{0}/{1}.png").format(size, icon)
697
    }
698

    
699
    var os_icon_tag = uihelpers.os_icon_tag = function (os, size, active, attrs) {
700
        attrs = attrs || {};
701
        return '<img src="{0}" />'.format(os_icon_path(os, size, active));
702
    }
703

    
704
    // VM Icon helpers
705
    //
706
    // identify icon
707
    var vm_icon = uihelpers.vm_icon = function(vm) {
708
        return os_icon(vm.get_os());
709
    }
710
    
711
    // get icon url
712
    var vm_icon_path = uihelpers.vm_icon_path = function(vm, size) {
713
        return os_icon_path(vm.get_os(), size, vm.is_active());
714
    }
715
    
716
    // get icon IMG tag
717
    var vm_icon_tag = uihelpers.vm_icon_tag = function (vm, size, attrs) {
718
       return os_icon_tag(vm.get_os(), size, vm.is_active(), attrs);
719
    }
720
    
721

    
722
    snf.ui = _.extend(snf.ui, bb.Events);
723
    snf.ui.trigger_error = function(code, msg, error, extra) {
724
        if (msg.match(/Script error/i)) {
725
          // No usefull information to display in this case. Plus it's an
726
          // exception that probably doesn't affect our app.
727
          return 0;
728
        }
729
        snf.ui.trigger("error", { code:code, msg:msg, error:error, extra:extra || {} });
730
    };
731

    
732
})(this);