Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / ui / static / snf / js / ui / web / ui_vm_resize_view.js @ 80bb2140

History | View | Annotate | Download (15.6 kB)

1
// Copyright 2013 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
    var util = snf.util = snf.util || {};
46

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

    
49
    // shortcuts
50
    var bb = root.Backbone;
51

    
52
    
53
    views.FlavorOptionsView = views.View.extend({
54

    
55
        choices_meta: {
56
            'cpu': {title: 'CPUs', description: 'Choose number of CPU cores'},
57
            'ram': {title: 'Memory size', description: 'Choose memory size'},
58
            'disk': {title: 'Disk size', description: 'Choose disk size'},
59
            'disk_template': {title: 'Storage', description: 'Select storage type'}
60
        },
61

    
62
        initialize: function(options) {
63
            views.FlavorOptionsView.__super__.initialize.apply(this);
64
            _.bindAll(this);
65
            this.$el = $(this.el);
66
            this.flavors = options.flavors || synnefo.storage.flavors.active();
67
            this.collection = options.collection || synnefo.storage.flavors;
68
            this.choices = options.choices || ['cpu', 'ram', 'disk', 
69
                                               'disk_template'];
70
            this.hidden_choices = options.hidden_choices || [];
71
            this.quotas = options.quotas || synnefo.storage.quotas;
72
            this.selected_flavor = options.selected_flavor || undefined;
73
            this.extra_quotas = options.extra_quotas || undefined;
74
            this.render();
75
            if (this.selected_flavor) { this.set_flavor(this.selected_flavor)}
76
        },
77

    
78
        render: function() {
79
            this.$el.empty();
80
            this.each_choice(function(choice) {
81
                var el = this[choice + '_el'] = $("<div>");
82
                el.addClass("flavor-options clearfix");
83
                el.addClass(choice);
84
                var title = $("<h4 class='clearfix'><span class='title'>" + 
85
                              "</span><span class='available'></span>" +
86
                              "<span class='desc'></span></h4>");
87
                el.append(title);
88
                el.find("span.title").text(this.choices_meta[choice].title);
89
                el.find("span.desc").text(this.choices_meta[choice].description);
90
                var ul = $("<ul/>");
91
                ul.addClass("flavors-"+choice+"-list flavor-opts-list clearfix");
92
                el.append(ul);
93
                this.$el.append(el);
94
                if (this.hidden_choices.indexOf(choice) > -1) {
95
                    el.hide();
96
                }
97
            });
98
            this.update_quota_left();
99
            this.fill();
100
            this.set_unavailable();
101
            this.init_handlers();
102
        },
103
        
104
        get_current: function(choice, value) {
105
          var found = false;
106
          _.each(this.flavors, _.bind(function(f){
107
              if (found) { return }
108
              if (f.get(choice) == value) {
109
                  found = true;
110
                  to_select = f;
111
              }
112
          }, this));
113
        },
114

    
115
        init_handlers: function() {
116
            this.$el.on('click', 'li.choice', _.bind(function(e) {
117
                var el = $(e.target).closest('li');
118
                var choice = el.data('type');
119
                var value = el.data('value');
120
                var to_select = this.selected_flavor;
121
                if (to_select) {
122
                    var attrs = _.clone(to_select.attributes);
123
                    attrs[choice] = value;
124
                    to_select = this.collection.get_flavor(attrs.cpu, attrs.ram, 
125
                                                           attrs.disk, 
126
                                                           attrs.disk_template);
127
                }
128

    
129
                if (!to_select) {
130
                  to_select = this.get_current(choice, value);
131
                }
132
                this.set_flavor(to_select);
133
            }, this));
134
        },
135
        
136
        update_quota_left: function() {
137
            this.each_choice(function(choice){
138
                var el = this[choice + '_el'].find(".available");
139
                el.removeClass("error");
140
                var quota = this.quotas.get('cyclades.'+choice);
141
                if (!quota) { return }
142
                var type = choice;
143
                var active = true;
144
                var key = 'available';
145
                var quotas = synnefo.storage.quotas;
146
                var available_dsp = quotas.get('cyclades.'+type).get_readable(key, active);
147
                var available = quotas.get('cyclades.'+type).get(key);
148
                var content = "({0} left)".format(available_dsp);
149
                if (available <= 0) { content = "(None left)"; el.addClass("error") }
150
                el.text(content);
151
            });
152
        },
153

    
154
        metric_for_choice: function(choice) {
155
            var map = {ram:'MB', cpu:'X', disk:'GB', disk_template:''};
156
            return map[choice] || '';
157
        },
158
        
159
        set_unavailable: function() {
160
            this.$el.find("li.choice").removeClass("disabled");
161
            var quotas = this.quotas.get_available_for_vm({'active': true});
162
            var extra_quotas = this.extra_quotas;
163
            var user_excluded = storage.flavors.unavailable_values_for_quotas(
164
              quotas, 
165
              storage.flavors.active(), extra_quotas);
166
            _.each(user_excluded, _.bind(function(values, key) {
167
                _.each(values, _.bind(function(value) {
168
                    var choice_el = this.select_choice(key, value);
169
                    choice_el.addClass("disabled").removeClass("selected");
170
                }, this));
171
            }, this));
172
        },
173

    
174
        select_choice: function(key, value) {
175
            return this.$el.find(".choice[data-type="+key+"][data-value="+value+"]");
176
        },
177

    
178
        fill: function(flavors) {
179
            var flavors = flavors || this.flavors;
180
            var data = this.collection.get_data(flavors);
181
            this.each_choice(function(choice) {
182
                var el = this[choice + '_el'].find("ul");
183
                el.empty();
184
                var key = choice;
185
                if (key == 'ram') { key = 'mem'}
186
                var values = data[key];
187
                if (!values) { return }
188
                _.each(values, _.bind(function(value) {
189
                    var entry = $("<li class='choice choice-" + choice + "' " +
190
                                  "data-value=" + value +
191
                                  " data-type=" + choice + ">" +
192
                                  "<span class='value'></span>" +
193
                                  "<span class='metric'></span></li>");
194
                    entry.find(".value").text(value);
195
                    entry.find(".metric").text(this.metric_for_choice(choice));
196
                    el.append(entry);
197
                    el.attr('value', value);
198
                }, this));
199
            });
200
        },
201

    
202
        set_flavor: function(flavor) {
203
            this.$el.find("li").removeClass("selected");
204
            if (!flavor) {this.selected_flavor = undefined; return}
205
            var no_select = false;
206
            var self = this;
207
            this.each_choice(function(choice){
208
                var el = this[choice + '_el'];
209
                var choice = el.find('.choice-'+choice+'[data-value='+flavor.get(choice)+']');
210
                choice.addClass("selected");
211
            });
212
            this.selected_flavor = flavor;
213
            this.trigger("flavor:select", this.selected_flavor);
214
            return this.selected_flavor;
215
        },
216

    
217
        each_choice: function(f) {
218
            return _.each(this.choices, _.bind(f, this));
219
        }
220
    });
221

    
222
    views.VmResizeView = views.Overlay.extend({
223
        
224
        view_id: "vm_resize_view",
225
        content_selector: "#vm-resize-overlay-content",
226
        css_class: 'overlay-vm-resize overlay-info',
227
        overlay_id: "vm-resize-overlay",
228

    
229
        subtitle: "",
230
        title: "Resize Machine",
231

    
232
        initialize: function(options) {
233
            this.flavors_view = undefined; 
234
            views.VmResizeView.__super__.initialize.apply(this);
235
            _.bindAll(this);
236
            this.submit = this.$(".form-action.resize");
237
            this.shutdown = this.$(".form-action.shutdown");
238
            this.pre_init_handlers();
239
            this.handle_shutdown_complete = _.bind(this.handle_shutdown_complete, this);
240
        },
241

    
242
        pre_init_handlers: function() {
243
            this.submit.click(_.bind(function(){
244
                if (this.submit.hasClass("disabled")) {
245
                    return;
246
                };
247
                this.submit_resize(this.flavors_view.selected_flavor);
248
            }, this));
249
            this.shutdown.click(_.bind(this.handle_shutdown, this));
250
        },
251
        
252
        handle_shutdown: function() {
253
          if (this.shutdown.hasClass("in-progress") || 
254
              this.shutdown.hasClass("disabled")) {
255
            return;
256
          }
257
          
258
          this.shutdown.addClass("in-progress");
259

    
260
          this.vm.unbind("change:status", this.handle_shutdown_complete);
261
          this.vm.bind("change:status", this.handle_shutdown_complete);
262

    
263
          var self = this;
264
          this.vm.call("shutdown", undefined, function() {
265
            self.shutdown.removeClass("in-progress");
266
            self.update_layout();
267
            self.hide();
268
          });
269
        },
270

    
271
        handle_shutdown_complete: function(vm) {
272
          if (!vm.is_active()) {
273
            this.shutdown.removeClass("in-progress");
274
            this.vm.unbind("change:status", this.handle_shutdown_complete);
275
          }
276
        },
277

    
278
        submit_resize: function(flv) {
279
            if (this.submit.hasClass("in-progress")) { return }
280
            this.submit.addClass("in-progress");
281
            var complete = _.bind(function() {
282
              this.vm.set({'flavor': flv});
283
              this.vm.set({'flavorRef': flv.id});
284
              this.hide();
285
            }, this);
286
            this.vm.call("resize", complete, complete, {flavor:flv.id});
287
        },
288
        
289
        show_with_warning: function(vm) {
290
          this.show(vm);
291
          this.start_warning.show();
292
        },
293

    
294
        show: function(vm) {
295
            this.start_warning = this.$(".warning.start").hide();
296
            this.start_warning.hide();
297
            this.submit.removeClass("in-progress");
298
            this.vm = vm;
299
            this.vm.bind("change", this.handle_vm_change);
300
            if (this.flavors_view) {
301
                this.flavors_view.remove();
302
            }
303
            this.warning = this.$(".warning.shutdown");
304
            this.warning.hide();
305
            this.submit.show();
306
            this.shutdown.removeClass("in-progress");
307
            this.$(".flavor-options-inner-cont").append("<div>");
308
            var extra_quota = this.vm.get_flavor_quotas();
309
            if (!this.vm.is_active()) {
310
              extra_quota = undefined;
311
            }
312
            this.flavors_view = new snf.views.FlavorOptionsView({
313
                flavors:this.vm.get_resize_flavors(),
314
                el: this.$(".flavor-options-inner-cont div"),
315
                hidden_choices:['disk', 'disk_template'],
316
                selected_flavor: this.vm.get_flavor(),
317
                extra_quotas: extra_quota
318
            });
319
            this.flavors_view.bind("flavor:select", this.handle_flavor_select)
320
            this.submit.addClass("disabled");
321
            views.VmResizeView.__super__.show.apply(this);
322
        },
323

    
324
        handle_flavor_select: function(flv) {
325
            this.selected_flavor = flv;
326
            if (!flv || (flv.id == this.vm.get_flavor().id)) {
327
                this.submit.addClass("disabled");
328
                if (!this.shutdown.hasClass("in-progress")) {
329
                  this.shutdown.addClass("disabled");
330
                }
331
            } else {
332
                if (this.vm.can_resize()) {
333
                  this.submit.removeClass("disabled");
334
                } else {
335
                  this.shutdown.removeClass("disabled hidden");
336
                  this.warning.show();
337
                }
338
            }
339
            if (flv && !this.vm.can_start(flv, true)) {
340
              if (!this.vm.is_active()) {
341
                this.start_warning.show();
342
              }
343
            } else {
344
              this.start_warning.hide();
345
            }
346
            this.update_vm_status();
347
        },
348

    
349
        update_vm_status: function() {
350
          if (this.vm.get("status") == "STOPPED") {
351
            this.warning.hide();
352
          }
353
          if (this.vm.get("status") == "SHUTDOWN") {
354
            this.shutdown.addClass("in-progress").removeClass("disabled");
355
            this.warning.hide();
356
          }
357
        },
358

    
359
        beforeOpen: function() {
360
            this.update_layout();
361
            this.init_handlers();
362
        },
363

    
364
        update_layout: function() {
365
            this.update_actions();
366
            this.update_vm_details();
367
            this.render_choices();
368
            this.update_vm_status();
369
        },
370

    
371
        update_actions: function() {
372
          if (!this.vm.can_resize()) {
373
            this.shutdown.show();
374
            this.warning.show();
375
            this.shutdown.removeClass("disabled");
376
            if (this.selected_flavor) {
377
              this.handle_flavor_select(this.selected_flavor);
378
            } else {
379
              if (!this.shutdown.hasClass("in-progress")) {
380
                this.shutdown.addClass("disabled");
381
              }
382
            }
383
            this.submit.addClass("disabled");
384
          } else {
385
            if (this.selected_flavor) {
386
              this.submit.removeClass("disabled");
387
            }
388
            this.shutdown.hide();
389
          }
390
        },
391
          
392
        render_choices: function() {
393
        },
394

    
395
        update_vm_details: function() {
396
            var name = _.escape(util.truncate(this.vm.get("name"), 70));
397
            this.set_subtitle(name + snf.ui.helpers.vm_icon_tag(this.vm, "small"));
398
        },
399

    
400
        handle_vm_change: function() {
401
          this.update_layout();
402
        },
403

    
404
        init_handlers: function() {
405
        },
406

    
407
        onClose: function() {
408
            this.editing = false;
409
            this.vm.unbind("change", this.handle_vm_change);
410
            this.vm.unbind("change:status", this.handle_shutdown_complete);
411
            this.vm = undefined;
412
        }
413
    });
414
    
415
})(this);
416