Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (22.2 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
        // Helpers
79
        //
80
        // get element based on this.selectors key/value pairs
81
        sel: function(id, params) {
82
            if (!this.selectors[id]){ return };
83
            return $(this.selectors[id].format(params));
84
        },
85
        
86
        // vm element based on vm model instance provided
87
        vm: function(vm) {
88
            if (hasKey.call(this._vm_els, vm.id)) {
89
                ret = this._vm_els[vm.id];
90
            } else {
91
                return $([]);
92
            }
93

    
94
            return ret;
95
        },
96
        
97
        // get vm model instance from DOM element
98
        vm_for_element: function(el) {
99
            return storage.vms.sel(this.vm_id_for_element(el));
100
        },
101
        
102

    
103
        // Event binding and stuff like that
104
        //
105
        set_storage_handlers: function() {
106
            storage.vms.bind("add", _.bind(this.vms_updated_handler, this, "add"));
107
            storage.vms.bind("change", _.bind(this.vms_updated_handler, this, "change"));
108
            storage.vms.bind("reset", _.bind(this.vms_updated_handler, this, "reset"));
109
            storage.vms.bind("remove", _.bind(this.vms_updated_handler, this, "remove"));
110
        },
111
        
112
        // vms updated triggered, update view vms
113
        vms_updated_handler: function (method, model, arg2, arg3) {
114
            var updated = storage.vms.models;
115
            if (method == "add") { updated = [model] };
116
            if (method == "change") { updated = [model] };
117
            if (method == "remove") { updated = [model] };
118

    
119
            if (method == "remove") {
120
                this.remove_vm(model)
121
                return;
122
            }
123
            
124
            this.update_vms(updated);
125
        },
126

    
127
        // create vm
128
        // append it on proper view container
129
        create_vm: function(vm) {
130
            // create dom element
131
            var vm_view = this.create_vm_element(vm);
132
            vm_view.find(".vm-actions").attr("id", this.view_id+"-actions-" + vm.id);
133
            this._vm_els[vm.id] = vm_view;
134
            var container = this.get_vm_container(vm);
135
            container.append(vm_view);
136
            vm_view.find(".action-indicator").text("");
137
            if (this.visible()) {
138
                container.show()
139
            }
140

    
141
            // initialize vm specific event handlers 
142
            this.__set_vm_handlers(vm);
143
            return vm_view;
144
        },
145
        
146
        // create vm dom element
147
        create_vm_element: function(vm) {
148
            // clone template
149
            return this.sel('tpl').clone().attr("id", this.id_tpl + vm.id)
150
        },
151

    
152
        // get proper vm container
153
        get_vm_container: function(vm) {
154
            if (vm.is_active()) {
155
                return this.sel("vm_cont_active");
156
            } else {
157
                return this.sel("vm_cont_terminated");
158
            }
159
        },
160

    
161
        // create and append inside the proper container the vm model
162
        // if it doesn't exist update vm data and make it visible
163
        add: function(vm) {
164
            // create if it does not exist
165
            if (!hasKey.call(this._vm_els, vm.id)) {
166
                var el = this.create_vm(vm);
167
                el.show();
168
                this.post_add(vm);
169
            }
170

    
171
            return this.vm(vm);
172
        },
173
        
174
        // helpers for VMListView descendants
175
        post_add: function(vm) { throw "Not implemented" },
176
        set_vm_handlers: function(vm) { throw "Not implemented" },
177
        set_handlers: function() { throw "Not implemented" },
178
        update_layout: function() { throw "Not implemented" },
179
        post_update_vm: function(vm) { throw "Not implemented" },
180
        update_details: function(vm) {},
181
        
182
        // remove vm
183
        remove_vm: function(vm) {
184
            // FIXME: some kind of transiton ??? effect maybe ???
185
            this.vm(vm).remove();
186
            this.post_remove_vm(vm);
187
            if (hasKey.call(this._vm_els, vm.id)) {
188
                delete this._vm_els[vm.id];
189
            }
190
        },
191
        
192
        // remove all vms from view
193
        clear: function() {
194
            this.sel('vms').remove();
195
            this.__update_layout();
196
        },
197

    
198
        show: function() {
199
            views.VMListView.__super__.show.apply(this, arguments);
200
            if (storage.vms.length == 0) { this.hide() };
201
            if (!snf.config.update_hidden_views) {
202
                this.update_vms(storage.vms.models);
203
            }
204
        },
205

    
206
        // do update for provided vms, then update the view layout
207
        update_vms: function(vms) {
208
            if (!this.visible() && !snf.config.update_hidden_views) { return };
209

    
210
            _.each(vms, _.bind(function(vm){
211
                // vm will be removed
212
                // no need to update
213
                if (vm.get("status") == "DELETED") {
214
                    return;
215
                }
216

    
217
                // this won't add it additional times
218
                this.add(vm);
219
                this.update_vm(vm);
220
            }, this))
221
            
222
            // update view stuff
223
            this.__update_layout();
224
        },
225
        
226
        // update ui for the given vm
227
        update_vm: function(vm) {
228
            // do not update deleted state vms
229
            if (!vm || vm.get("status") == 'DELETED') { return };
230
            this.check_vm_container(vm);
231

    
232
            this.update_details(vm);
233
            this.update_transition_state(vm);
234

    
235
            if (this.action_views) {
236
                this.action_views[vm.id].update();
237
                this.action_views[vm.id].update_layout();
238
            }
239
            
240
            try {
241
                this.post_update_vm(vm);
242
            } catch (err) {};
243
        },
244

    
245
        // check if vm is placed properly within the view
246
        // container (e.g. some views might have different
247
        // containers for terminated or running machines
248
        check_vm_container: function(vm){
249
            if (vm.state() == "DESTROY") { return };
250
            var el = this.vm(vm);
251
            if (!el.length) { return };
252
            var self = this;
253
            var selector = vm.is_active() ? 'vm_cont_active' : 'vm_cont_terminated';
254
            if (el.parent()[0] != this.sel(selector)[0]) {
255
                var cont = this.sel(selector);
256
                var self = this;
257

    
258
                el.hide().appendTo(cont).fadeIn(300);
259
                $(window).trigger('resize');
260

    
261
                //el.fadeOut(200, function() {
262
                    //el.appendTo(cont); 
263
                    //el.fadeIn(200);
264
                    //self.sel(selector).show(function(){
265
                        //$(window).trigger("resize");
266
                    //});
267
                //});
268
            }
269
        },
270

    
271
        __update_layout: function() {
272
            this.update_layout();
273
        },
274
        
275
        // append handlers for vm specific events
276
        __set_vm_handlers: function(vm) {
277
            // show transition on vm status transit
278
            vm.bind('transition', _.bind(function(){this.show_transition(vm)}, this));
279
            this.set_vm_handlers(vm);
280
        },
281
        
282
        // is vm in transition ??? show the progress spinner
283
        update_transition_state: function(vm) {
284
            if (vm.in_transition() && !vm.pending_action){
285
                this.sel('vm_spinner', vm.id).show();
286
            } else {
287
                this.sel('vm_spinner', vm.id).hide();
288
            }
289
        },
290
        
291
        show_indicator: function(vm, action) {
292
            var action = action || vm.pending_action;
293
            this.sel('vm_wave', vm.id).hide();
294
            this.sel('vm_spinner', vm.id).hide();
295
            this.vm(vm).find(".action-indicator").removeClass().addClass(action + " action-indicator").show();
296
        },
297

    
298
        hide_indicator: function(vm) {
299
            this.vm(vm).find(".action-indicator").removeClass().addClass("action-indicator").hide();
300
            this.update_transition_state(vm);
301
        },
302

    
303
        // display transition animations
304
        show_transition: function(vm) {
305
            var wave = this.sel('vm_wave', vm.id);
306
            if (!wave || !wave.length) { return }
307

    
308
            var src = wave.attr('src');
309
            // change src to force gif play from the first frame
310
            // animate for 500 ms then hide
311
            wave.attr('src', "").show();
312
            wave.attr('src', src).fadeIn(200).delay(700).fadeOut(300, function() {
313
                wave.hide();
314
            });
315
        },
316

    
317
        connect_to_console: function(vm) {
318
            // It seems that Safari allows popup windows only if the window.open
319
            // call is made within an html element click event context. 
320
            // Otherwise its behaviour is based on "Block Pop-Up Windows" 
321
            // setting, which when enabled no notification appears for the user. 
322
            // Since there is no easy way to check for the setting value we use
323
            // async:false for the action call so that action success handler 
324
            // which opens the new window is called inside the click event 
325
            // context.
326
            var use_async = true;
327
            if ($.client.browser == "Safari") {
328
                use_async = false;
329
            }
330

    
331
            vm.call("console", function(console_data) {
332
                var url = vm.get_console_url(console_data);
333
                snf.util.open_window(url, "VM_" + vm.get("id") + "_CONSOLE", {});
334
            }, undefined, {async: use_async});
335
        }
336

    
337
    });
338
    
339
    // empty message view (just a wrapper to the element containing 
340
    // the empty information message)
341
    views.EmptyView = views.View.extend({
342
        el: '#emptymachineslist'
343
    })
344

    
345
    views.VMActionsView = views.View.extend({
346
        
347
        initialize: function(vm, parent, el, hide) {
348
            this.hide = hide || false;
349
            this.view = parent;
350
            this.vm = vm;
351
            this.vm_el = el;
352
            this.el = $("#" + parent.view_id + "-actions-" + vm.id);
353
            this.all_action_names = _.keys(views.VMActionsView.STATUS_ACTIONS);
354
            
355
            // state params
356
            this.selected_action = false;
357

    
358
            _.bindAll(this);
359
            window.acts = this;
360
            this.view_id = "vm_" + vm.id + "_actions";
361
            views.VMActionsView.__super__.initialize.call(this);
362

    
363
            this.hovered = false;
364
            this.set_hover_handlers();
365
        },
366

    
367
        action: function(name) {
368
            return $(this.el).find(".action-container." + name);
369
        },
370

    
371
        action_link: function(name) {
372
            return this.action(name).find("a");
373
        },
374
        
375
        action_confirm_cont: function(name) {
376
            return this.action_confirm(name).parent();
377
        },
378

    
379
        action_confirm: function(name) {
380
            return this.action(name).find("button.yes");
381
        },
382

    
383
        action_cancel: function(name) {
384
            return this.action(name).find("button.no");
385
        },
386

    
387
        hide_actions: function() {
388
            $(this.el).find("a").css("visibility", "hidden");
389
        },
390

    
391
        // update the actions layout, depending on the selected actions
392
        update_layout: function() {
393

    
394
            if (!this.vm_handlers_initialized) {
395
                this.vm = storage.vms.get(this.vm.id);
396
                this.init_vm_handlers();
397
            }
398

    
399
            if (!this.vm) { return }
400
            
401
            if (!this.hovered && !this.vm.has_pending_action() && this.hide && !this.vm.action_error) { 
402
                this.el.hide();
403
                this.view.hide_indicator(this.vm);
404
                return 
405
            };
406

    
407
            if (!this.el.is(":visible") && !this.hide) { return };
408
            if (!this.handlers_initialized) { this.set_handlers(); }
409

    
410

    
411
            // update selected action
412
            if (this.vm.pending_action) {
413
                this.selected_action = this.vm.pending_action;
414
            } else {
415
                this.selected_action = false;
416
            }
417
            
418
            // vm actions tha can be performed
419
            var actions = this.vm.get_available_actions();
420
            
421
            // had pending action but actions changed and now selected action is
422
            // not available, hide it from user
423
            if (this.selected_action && actions.indexOf(this.selected_action) == -1) {
424
                this.reset();
425
            }
426
            
427
            this.el.show();
428
            
429
            if ((this.selected_action || this.hovered) && !this.vm.action_error) {
430
                // show selected action
431
                $(this.el).show();
432
                $(this.el).find("a").css("visibility", "visible");
433
                // show action icon
434
                this.view.show_indicator(this.vm);
435
            } else {
436
                if (this.hide || this.vm.action_error) {
437
                    // view shows actions on machine hover
438
                    $(this.el).find("a").css("visibility", "hidden");
439
                } else {
440
                    if (!this.vm.action_error) {
441
                        // view shows actions always
442
                        $(this.el).find("a").css("visibility", "visible");
443
                        $(this.el).show();
444
                    }
445
                }
446
                
447
                this.view.hide_indicator(this.vm);
448
            }
449
                
450
            // update action link styles and shit
451
            _.each(models.VM.ACTIONS, function(action, index) {
452
                if (actions.indexOf(action) > -1) {
453
                    this.action(action).removeClass("disabled");
454
                    if (this.selected_action == action) {
455
                        this.action_confirm_cont(action).css('display', 'block');
456
                        this.action_confirm(action).show();
457
                        this.action(action).removeClass("disabled");
458
                        this.action_link(action).addClass("selected");
459
                    } else {
460
                        this.action_confirm_cont(action).hide();
461
                        this.action_confirm(action).hide();
462
                        this.action_link(action).removeClass("selected");
463
                    }
464
                } else {
465
                    this.action().hide();
466
                    this.action(action).addClass("disabled");
467
                    this.action_confirm(action).hide();
468
                }
469
            }, this);
470
        },
471
        
472
        init_vm_handlers: function() {
473
            try {
474
                this.vm.unbind("action:fail", this.update_layout)
475
                this.vm.unbind("action:fail:reset", this.update_layout)
476
            } catch (err) { console.log("Error")};
477
            
478
            this.vm.bind("action:fail", this.update_layout)
479
            this.vm.bind("action:fail:reset", this.update_layout)
480

    
481
            this.vm_handlers_initialized = true;
482
        },
483
        
484
        set_hover_handlers: function() {
485
            // vm container hover (icon view)
486
            this.view.vm(this.vm).hover(_.bind(function() {
487
                this.hovered = true;
488
                this.update_layout();
489

    
490
            }, this),  _.bind(function() {
491
                this.hovered = false;
492
                this.update_layout();
493
            }, this));
494
        },
495

    
496
        // bind event handlers
497
        set_handlers: function() {
498
            var self = this;
499
            var vm = this.vm;
500
            
501
            // initial hide
502
            if (this.hide) { $(this.el).hide() };
503
            
504
            // action links events
505
            _.each(models.VM.ACTIONS, function(action) {
506
                var action = action;
507
                // indicator hovers
508
                this.view.vm(this.vm).find(".action-container."+action+" a").hover(function() {
509
                    self.view.show_indicator(self.vm, action);
510
                }, function() {
511
                    // clear or show selected action indicator
512
                    if (self.vm.pending_action) {
513
                        self.view.show_indicator(self.vm);
514
                    } else {
515
                        self.view.hide_indicator(self.vm);
516
                    }
517
                })
518
                
519
                // action links click events
520
                $(this.el).find(".action-container."+action+" a").click(function(ev) {
521
                    ev.preventDefault();
522
                    self.set(action);
523
                }).data("action", action);
524

    
525
                // confirms
526
                $(this.el).find(".action-container."+action+" button.no").click(function(ev) {
527
                    ev.preventDefault();
528
                    self.reset();
529
                });
530

    
531
                // cancels
532
                $(this.el).find(".action-container."+action+" button.yes").click(function(ev) {
533
                    ev.preventDefault();
534
                    // override console
535
                    // ui needs to act (open the console window)
536
                    if (action == "console") {
537
                        self.view.connect_to_console(self.vm);
538
                    } else {
539
                        self.vm.call(action);
540
                    }
541
                    self.reset();
542
                });
543
            }, this);
544

    
545
            this.handlers_initialized = true;
546
        },
547
        
548
        // reset actions
549
        reset: function() {
550
            var prev_action = this.selected_action;
551
            this.selected_action = false;
552
            this.vm.clear_pending_action();
553
            this.trigger("change", {'action': prev_action, 'vm': this.vm, 'view': this, remove: true});
554
            this.trigger("remove", {'action': prev_action, 'vm': this.vm, 'view': this, remove: true});
555
        },
556
        
557
        // set selected action
558
        set: function(action_name) {
559
            this.selected_action = action_name;
560
            this.vm.update_pending_action(this.selected_action);
561
            this.view.vm(this.vm).find(".action-indicator").show().removeClass().addClass(action_name + " action-indicator");
562
            this.trigger("change", {'action': this.selected_action, 'vm': this.vm, 'view': this});
563
        },
564

    
565
        update: function() {
566
        }
567
    })
568

    
569

    
570
    views.VMActionsView.STATUS_ACTIONS = { 
571
        'reboot':        ['UNKOWN', 'ACTIVE', 'REBOOT'],
572
        'shutdown':      ['UNKOWN', 'ACTIVE', 'REBOOT'],
573
        'console':       ['ACTIVE'],
574
        'start':         ['UNKOWN', 'STOPPED'],
575
        'destroy':       ['UNKOWN', 'ACTIVE', 'STOPPED', 'REBOOT', 'ERROR', 'BUILD']
576
    };
577

    
578
    // UI helpers
579
    var uihelpers = snf.ui.helpers = {};
580
    
581
    // OS icon helpers
582
    var os_icon = uihelpers.os_icon = function(os) {
583
        var icons = window.os_icons;
584
        if (!icons) { return "okeanos" }
585
        if (icons.indexOf(os) == -1) {
586
            os = "okeanos";
587
        }
588
        return os;
589
    }
590

    
591
    var os_icon_path = uihelpers.os_icon_path = function(os, size, active) {
592
        size = size || "small";
593
        if (active == undefined) { active = true };
594

    
595
        var icon = os_icon(os);
596
        if (active) {
597
            icon = icon + "-on";
598
        } else {
599
            icon = icon + "-off";
600
        }
601

    
602
        return (snf.config.machines_icons_url + "{0}/{1}.png").format(size, icon)
603
    }
604

    
605
    var os_icon_tag = uihelpers.os_icon_tag = function (os, size, active, attrs) {
606
        attrs = attrs || {};
607
        return '<img src="{0}" />'.format(os_icon_path(os, size, active));
608
    }
609

    
610
    // VM Icon helpers
611
    //
612
    // identify icon
613
    var vm_icon = uihelpers.vm_icon = function(vm) {
614
        return os_icon(vm.get_os());
615
    }
616
    
617
    // get icon url
618
    var vm_icon_path = uihelpers.vm_icon_path = function(vm, size) {
619
        return os_icon_path(vm.get_os(), size, vm.is_active());
620
    }
621
    
622
    // get icon IMG tag
623
    var vm_icon_tag = uihelpers.vm_icon_tag = function (vm, size, attrs) {
624
       return os_icon_tag(vm.get_os(), size, vm.is_active(), attrs);
625
    }
626
    
627

    
628
    snf.ui = _.extend(snf.ui, bb.Events);
629
    snf.ui.trigger_error = function(code, msg, error, extra) {
630
        snf.ui.trigger("error", { code:code, msg:msg, error:error, extra:extra || {} })
631
    };
632

    
633
})(this);