Revision 42f67a2a

b/ui/static/synnefo.js
1
function list_view() {
2
    $.cookie("list", '1'); // set list cookie
3
    $("div#machinesview").load($("#list").attr("href"), function(){
4
        $("a#standard")[0].className += ' activelink';
5
        $("a#list")[0].className = '';
6
    });
7
    return false;
8
}
9

  
10
function standard_view() {
11
    $.cookie("list", '0');
12
    href=$("a#standard").attr("href");
13
    $("div#machinesview").load(href, function(){
14
        $("a#list")[0].className += ' activelink';
15
        $("a#standard")[0].className = '';
16
    });
17
    return false;
18
}
19

  
20
function choose_view() {
21
    if ($.cookie("list")=='1') {
22
        list_view();
23
    } else {
24
        standard_view();
25
    }
26
}
27

  
28
function toggleMenu() {
29
    var primary = $("ul.css-tabs li a.primary");
30
    var secondary = $("ul.css-tabs li a.secondary");
31
    var all = $("ul.css-tabs li a");			
32
    var toggled = $('ul.css-tabs li a.current').hasClass('secondary');
33
    
34
    // if anything is still moving, do nothing
35
    if ($(":animated").length) {
36
        return;
37
    } 
38
    
39
    // nothing is current to begin with
40
    $('ul.css-tabs li a.current').removeClass('current');
41
    
42
    // move stuff around
43
    all.animate({top:'30px'}, {complete: function() { 
44
        $(this).hide();
45
        if (toggled) {
46
            primary.show();
47
            primary.animate({top:'9px'}, {complete: function() {
48
                $('ul.css-tabs li a.primary#machines').addClass('current');
49
                $('a#machines').click();                           	
50
            }});
51
        } else {
52
            secondary.show();
53
            secondary.animate({top:'9px'}, {complete: function() {
54
                $('ul.css-tabs li a.secondary#files').addClass('current');
55
                $('a#files').click();                           			                	
56
            }});
57
        }            	                          
58
    }});
59
    
60
    // rotate arrow icon
61
    if (toggled) {
62
        $("#arrow").rotate({animateAngle: (0), bind:[{"click":function(){toggleMenu()}}]});
63
        $("#arrow").rotateAnimation(0);            	
64
    } else {
65
        $("#arrow").rotate({animateAngle: (-180), bind:[{"click":function(){toggleMenu()}}]});
66
        $("#arrow").rotateAnimation(-180);
67
    }            
68
}
69

  
70
// confirmation overlay generation
71
function confirm_action(action_string, action_function, serverIDs, serverNames) {
72
    if (serverIDs.length == 1){
73
        $("#yes-no h3").text('You are about to ' + action_string + ' vm ' + serverNames[0]);
74
    } else if (serverIDs.length > 1){
75
        $("#yes-no h3").text('You are about to ' + action_string + ' ' + serverIDs.length + 'machines');
76
    } else {
77
        return false;
78
    }
79
    // action confirmation overlay
80
    var triggers = $("a#confirmation").overlay({
81
	    // some mask tweaks suitable for modal dialogs
82
	    mask: {
83
		    color: '#ebecff',
84
		    opacity: '0.9'
85
	    },
86
        top: 'center',
87
        load: false
88
    });
89
    // yes or no?
90
    var buttons = $("#yes-no button").click(function(e) {
91
	    // get user input
92
	    var yes = buttons.index(this) === 0;
93
        //close the confirmation window
94
        $("a#confirmation").overlay().close(); 
95
        // return true=yes or false=no
96
        if (yes) {
97
            action_function(serverIDs);
98
        } else {
99
            // reload page
100
            choose_view();
101
        }
102
    });
103
    $("a#confirmation").data('overlay').load();
104
    return false;
105
}
106

  
107
// get and show a list of running and terminated machines
108
function update_vms() {
109
    console.info('updating machines');
110
    $(".running").text('');
111
    $(".terminated").text('');
112

  
113
    $.ajax({
114
        url: '/api/v1.0/servers/detail',
115
        type: "GET",
116
        timeout: TIMEOUT,
117
        dataType: "json",
118
        error: function(jqXHR, textStatus, errorThrown) { 
119
                    ajax_error(jqXHR);
120
                    return false;
121
                    },
122
        success: function(data, textStatus, jqXHR) {
123
            if ($(".running a.name").length + $(".terminated a.name").length == 0) {
124
            
125
                $.each(data.servers, function(i,server){
126
                    // if the machine is deleted it should not be included in any list
127
                    if (server.status == 'DELETED') {
128
                        return;
129
                    }
130
                    var machine = $("#machine-template").clone().attr("id", server.id).fadeIn("slow");
131
                    machine.find("input[type='checkbox']").attr("id", "input-" + server.id);
132
                    machine.find("input[type='checkbox']").attr("class", server.status);
133
                    machine.find("a.name span.name").text(server.name);
134
                    machine.find("img.logo").attr("src","static/machines/"+image_tags[server.imageId]+'.png');
135
                    machine.find("img.list-logo").attr("src","static/os_logos/"+image_tags[server.imageId]+'.png');
136
                    machine.find("img.list-logo").attr("title",image_tags[server.imageId]);
137
                    machine.find("span.imagetag").text(image_tags[server.imageId]);
138
    
139
                    machine.find("a.ip span.public").text(String(server.addresses.public.ip.addr).replace(',',' '));            
140
    
141
                    // TODO: handle SHARE_IP, SHARE_IP_NO_CONFIG, DELETE_IP, REBUILD, QUEUE_RESIZE, PREP_RESIZE, RESIZE, VERIFY_RESIZE, PASSWORD, RESCUE
142
                    if (server.status == 'BUILD'){
143
                        machine.find(".status").text('Building');
144
                        machine.appendTo(".running");
145
                    } else if (server.status == 'ACTIVE') {
146
                        machine.find(".status").text('Running');
147
                        machine.appendTo(".running"); 
148
                    } else if (server.status == 'REBOOT' || server.status == 'HARD_REBOOT') {
149
                        machine.find(".status").text('Rebooting');
150
                        machine.appendTo(".running");
151
                    } else if (server.status == 'STOPPED') {
152
                        machine.find(".status").text('Stopped');
153
                        machine.find("img.logo").attr("src","static/machines/"+image_tags[server.imageId]+'-off.png');
154
                        machine.find("img.list-logo").attr("src","static/os_logos/"+image_tags[server.imageId]+'-off.png');
155
                        machine.appendTo(".terminated");
156
                    } else if (server.status == 'ERROR') {
157
                        machine.find(".status").text('Error');
158
                        machine.find("img.logo").attr("src","static/machines/"+image_tags[server.imageId]+'-off.png');
159
                        machine.find("img.list-logo").attr("src","static/os_logos/"+image_tags[server.imageId]+'-off.png');
160
                        machine.appendTo(".terminated");
161
                    } 
162
                    else {
163
                        machine.find(".status").text('Unknown');
164
                        machine.find("img.logo").attr("src","static/machines/"+image_tags[server.imageId]+'-off.png');
165
                        machine.find("img.list-logo").attr("src","static/os_logos/"+image_tags[server.imageId]+'-off.png');
166
                        machine.appendTo(".terminated");
167
                    }
168
                });
169
            }
170
            $("#spinner").hide();
171
            $("div.machine:last-child").find("div.seperator").hide();
172
            // if the terminated list is populated then the seperator must be shown
173
            if ($(".terminated a.name").length > 0) {
174
                $("#mini.seperator").fadeIn("slow");
175
            }
176
            // creating the table in list view, if there are machines to show
177
            if ($("div.list table.list-machines tbody").length > 0) {
178
                $("div.list table.list-machines").dataTable({
179
                    "bInfo": false,
180
                    "bPaginate": false,
181
            		"bAutoWidth": false,
182
            		"bSort": true,    
183
                    "bStateSave": true,
184
                    //"sScrollY": "250px",
185
                    //"sScrollX": "500px",
186
                    //"sScrollXInner": "480px",
187
                    "aoColumnDefs": [
188
                        { "bSortable": false, "aTargets": [ 0 ] }
189
                    ]
190
                });
191
                $("div.list table.list-machines").show();
192
                $("div.list div.actions").show();
193
            }
194
        }
195
    });
196
    return false;
197
}
198

  
199
// get and show a list of anvailable standard and custom images
200
function update_images() { 
201
    $.ajax({
202
        url: '/api/v1.0/images/detail',
203
        type: "GET",
204
        //async: false,
205
        dataType: "json",
206
        timeout: TIMEOUT,
207
        error: function(jqXHR, textStatus, errorThrown) { 
208
                    ajax_error(jqXHR);
209
                    },
210
        success: function(data, textStatus, jqXHR) {
211
            if ($("ul#standard-images li").toArray().length + $("ul#custom-images li").toArray().length == 0) {
212
                $.each(data.images, function(i,image){
213
                    var img = $('#image-template').clone().attr("id","img-"+image.id).fadeIn("slow");
214
                    img.find("label").attr('for',"img-radio-" + image.id);
215
                    img.find(".image-title").text(image.name);
216
                    img.find(".description").text(image.description);
217
                    img.find(".size").text(image.size);
218
                    img.find("input.radio").attr('id',"img-radio-" + image.id);
219
                    if (i==0) img.find("input.radio").attr("checked","checked"); 
220
                    img.find("img.image-logo").attr('src','static/os_logos/'+image_tags[image.id]+'.png');
221
                    if (image.serverId) {
222
                        img.appendTo("ul#custom-images");
223
                    } else {
224
                        img.appendTo("ul#standard-images");
225
                    }
226
                });
227
            }
228
        }
229
    });
230
    return false;
231
}
232

  
233
var flavors = {}, disks = [], cpus = [], ram = [];
234

  
235
Array.prototype.unique = function () {
236
	var r = new Array();
237
	o:for(var i = 0, n = this.length; i < n; i++)
238
	{
239
		for(var x = 0, y = r.length; x < y; x++)
240
		{
241
			if(r[x]==this[i])
242
			{
243
				continue o;
244
			}
245
		}
246
		r[r.length] = this[i];
247
	}
248
	return r;
249
}
250

  
251
// get and configure flavor selection
252
function update_flavors() { 
253
    $.ajax({
254
        url: '/api/v1.0/flavors/detail',
255
        type: "GET",
256
        //async: false,
257
        dataType: "json",
258
        timeout: TIMEOUT,
259
        error: function(jqXHR, textStatus, errorThrown) { 
260
            ajax_error(jqXHR);
261
        },
262
        success: function(data, textStatus, jqXHR) {
263
            flavors = data.flavors;
264
            $.each(flavors, function(i, flavor) {
265
                cpus[i] = flavor['cpu'];
266
                disks[i] = flavor['disk'];
267
                ram[i] = flavor['ram'];
268
            });
269
            cpus = cpus.unique();
270
            disks = disks.unique();
271
            ram = ram.unique();
272
            // sliders for selecting VM flavor
273
            $("#cpu:range").rangeinput({min:0,
274
                                       value:0,
275
                                       step:1,
276
                                       progress: true,
277
                                       max:cpus.length-1});
278
            
279
            $("#storage:range").rangeinput({min:0,
280
                                       value:0,
281
                                       step:1,
282
                                       progress: true,
283
                                       max:disks.length-1});
284

  
285
            $("#ram:range").rangeinput({min:0,
286
                                       value:0,
287
                                       step:1,
288
                                       progress: true,
289
                                       max:ram.length-1});
290
            $("#small").click();
291
            
292
            // update the indicators when sliding
293
            $("#cpu:range").data().rangeinput.onSlide(function(event,value){
294
                $("#cpu-indicator")[0].value = cpus[Number(value)];
295
                $("#custom").click();
296
            });
297
            $("#ram:range").data().rangeinput.onSlide(function(event,value){
298
                $("#ram-indicator")[0].value = ram[Number(value)];
299
                $("#custom").click();
300
            });
301
            $("#storage:range").data().rangeinput.onSlide(function(event,value){
302
                $("#storage-indicator")[0].value = disks[Number(value)];
303
                $("#custom").click();
304
            });
305
        }
306
    });
307
    return false;
308
}
309
// return flavorId from cpu, disk, ram values
310
function identify_flavor(cpu, disk, ram){
311
    for (i=0;i<flavors.length;i++){
312
        if (flavors[i]['cpu'] == cpu && flavors[i]['disk']==disk && flavors[i]['ram']==ram) {
313
            return flavors[i]['id']
314
        }
315
    }
316
    return 0;
317
}
318

  
319
// reboot action
320
function reboot(serverIDs){
321
	if (!serverIDs.length){
322
		ajax_success();
323
		return false;
324
	}	
325
    // ajax post reboot call
326
    var payload = {
327
        "reboot": {"type" : "HARD"}
328
    };
329
    serverID = serverIDs.pop();
330
	
331
	$.ajax({
332
		url: '/api/v1.0/servers/' + serverID + '/action',
333
		type: "POST",        
334
		dataType: "json",
335
		data: JSON.stringify(payload),
336
		timeout: TIMEOUT,
337
		error: function(jqXHR, textStatus, errorThrown) {
338
					ajax_error(jqXHR, serverID);
339
				},
340
		success: function(data, textStatus, jqXHR) {
341
					if ( jqXHR.status != '202') {
342
						ajax_error(jqXHR, serverID);
343
					} else {
344
						console.info('rebooted ' + serverID);        		
345
						reboot(serverIDs);
346
					}
347
				}
348

  
349
    });
350
	
351
    return false;
352
}
353

  
354
// shutdown action
355
function shutdown(serverIDs) {
356
	if (!serverIDs.length){
357
		ajax_success();
358
		return false;
359
	}
360
    // ajax post shutdown call
361
    var payload = {
362
        "shutdown": {"timeout" : "5"}
363
    };   
364

  
365
	serverID = serverIDs.pop()
366
    $.ajax({
367
	    url: '/api/v1.0/servers/' + serverID + '/action',
368
	    type: "POST",
369
	    dataType: "json",
370
        data: JSON.stringify(payload),
371
        timeout: TIMEOUT,
372
        error: function(jqXHR, textStatus, errorThrown) { 
373
                    ajax_error(jqXHR);
374
                    },
375
        success: function(data, textStatus, jqXHR) {
376
                    if ( jqXHR.status == '202') {
377
						console.info('suspended ' + serverID);        				
378
                        shutdown(serverIDs);
379
                    } else {
380
                        ajax_error(jqXHR);
381
                    }}             
382
    });
383
    return false;    
384
}
385

  
386
// destroy action
387
function destroy(serverIDs) {
388
	if (!serverIDs.length){
389
		ajax_success();
390
		return false;
391
	}
392
    // ajax post shutdown call
393
    var payload = {
394
        "shutdown": {"timeout" : "5"}
395
    };   
396

  
397
	serverID = serverIDs.pop()
398
    $.ajax({
399
	    url: '/api/v1.0/servers/' + serverID + '/action',
400
	    type: "DELETE",
401
	    dataType: "json",
402
        data: JSON.stringify(payload),
403
        timeout: TIMEOUT,
404
        error: function(jqXHR, textStatus, errorThrown) { 
405
                    ajax_error(jqXHR);
406
                    },
407
        success: function(data, textStatus, jqXHR) {
408
                    if ( jqXHR.status == '202') {
409
						console.info('suspended ' + serverID);        				
410
                        shutdown(serverIDs);
411
                    } else {
412
                        ajax_error(jqXHR);
413
                    }}             
414
    });
415
    return false;    
416
}
417

  
418
// start action
419
function start(serverIDs){
420
	if (!serverIDs.length){
421
		ajax_success();
422
		return false;
423
	}	
424
    // ajax post start call
425
    var payload = {
426
        "start": {"type" : "NORMAL"}
427
    };   
428

  
429
	serverID = serverIDs.pop()
430
    $.ajax({
431
        url: '/api/v1.0/servers/' + serverID + '/action',
432
        type: "POST",
433
        dataType: "json",
434
        data: JSON.stringify(payload),
435
        timeout: TIMEOUT,
436
        error: function(jqXHR, textStatus, errorThrown) { 
437
                    ajax_error(jqXHR);
438
                    },
439
        success: function(data, textStatus, jqXHR) {
440
                    if ( jqXHR.status == '202') {
441
					    console.info('started ' + serverID);        		
442
                        start(serverIDs);
443
                    } else {
444
                        ajax_error(jqXHR);
445
                    }}
446
    });
447
    return false;
448
}
b/ui/templates/home.html
7 7
    <script src="static/jquery.cookie.js"></script>
8 8
    <script src="static/jQueryRotate.js"></script>
9 9
    <script src="static/jquery.dataTables.min.js"></script>
10
    <script>
11
        /* These have to be here for the translations to work */
12
        // ajax error checking  
13
        function ajax_error(jqXHR) {
14
            // prepare the error message
15
            $("#error-success h3").text('{% trans "Error!" %}');
16
            // check the error code
17
            switch (jqXHR.status) {
18
                case 400: // YY error/message
19
                    $("#error-success p").text('{% trans "A Bad Request has been made." %}');
20
                    break;
21
                case 404: // YY error/message
22
                    $("#error-success p").text('{% trans "Your request has failed." %}');
23
                    break;
24
                case 501: // XX error/message
25
                    $("#error-success p").text('{% trans "There has been an Internal Error. Our administrators have been notified." %}');
26
                    break;
27
                case 503: // XX error/message
28
                    $("#error-success p").text('{% trans "This service is unavailable right now, please try again later." %}');
29
                    break;
30
                default: // XXYY error/message
31
                    $("#error-success p").text('{% trans "An error has happened. Our administrators have been notified." %}');
32
            }         
33
            // bring up error notification
34
            var triggers = $("a#notification").overlay({
35
                // some mask tweaks suitable for modal dialogs
36
                mask: {
37
                    color: '#ebecff',
38
                    opacity: '0.9'
39
                },
40
                top: 'center',
41
                closeOnClick: false,
42
                oneInstance: false,
43
                load: false,
44
                onClose: function(){
45
                    choose_view();
46
                }
47
            });
48
            $("a#notification").data('overlay').load();
49
            return false;
50
        }
51
        
52
        // ajax success checking
53
        function ajax_success() {          
54
            // prepare the error message
55
            $("#error-success h3").text('{% trans "Success!" %}');
56
            $("#error-success p").text('{% trans "Your request has been succefully executed." %}');             
57
            // bring up success notification
58
            var triggers = $("a#notification").overlay({
59
                // some mask tweaks suitable for modal dialogs
60
                mask: {
61
                    color: '#ebecff',
62
                    opacity: '0.9'
63
                },
64
                top: 'center',
65
                closeOnClick: false,
66
                oneInstance: false,
67
                load: false,
68
                onClose: function(){
69
                    choose_view();
70
                }
71
            });
72
            $("a#notification").data('overlay').load();
73
            return false;
74
        }
75
    
76
    </script>
77
    <script src="static/synnefo.js"></script>
10 78

  
11 79
	<link rel="stylesheet" type="text/css" href="static/main.css"/>	
12 80

  
......
73 141
			        var pane = this.getPanes().eq(i);
74 142
                    pane.text('');
75 143
			        // load it with a page specified in the tab's href attribute
76
			        pane.load(this.getTabs().eq(i).attr("href"));
77

  
144
			        pane.load(this.getTabs().eq(i).attr("href"),function(){if (!i) {choose_view()}});
78 145
		        }
79 146
	        });
80 147
        });
......
82 149
        // toggle main menu
83 150
        $("#arrow").click(function(event){
84 151
        	toggleMenu();
85
        }); 
86
                
87
		function toggleMenu() {
88
        	var primary = $("ul.css-tabs li a.primary");
89
            var secondary = $("ul.css-tabs li a.secondary");
90
            var all = $("ul.css-tabs li a");			
91
			var toggled = $('ul.css-tabs li a.current').hasClass('secondary');
92
			
93
			// if anything is still moving, do nothing
94
        	if ($(":animated").length) {
95
        		return;
96
        	} 
97
        	
98
        	// nothing is current to begin with
99
            $('ul.css-tabs li a.current').removeClass('current');
100
			
101
			// move stuff around
102
            all.animate({top:'30px'}, {complete: function() { 
103
                $(this).hide();
104
                if (toggled) {
105
	                primary.show();
106
	                primary.animate({top:'9px'}, {complete: function() {
107
	                	$('ul.css-tabs li a.primary#machines').addClass('current');
108
	                	$('a#machines').click();                           	
109
	                }});
110
                } else {
111
                	secondary.show();
112
                	secondary.animate({top:'9px'}, {complete: function() {
113
	                	$('ul.css-tabs li a.secondary#files').addClass('current');
114
	                	$('a#files').click();                           			                	
115
                	}});
116
                }            	                          
117
            }});
118
            
119
            // rotate arrow icon
120
            if (toggled) {
121
            	$("#arrow").rotate({animateAngle: (0), bind:[{"click":function(){toggleMenu()}}]});
122
            	$("#arrow").rotateAnimation(0);            	
123
            } else {
124
            	$("#arrow").rotate({animateAngle: (-180), bind:[{"click":function(){toggleMenu()}}]});
125
            	$("#arrow").rotateAnimation(-180);
126
            }            
127
		}  
152
        });    
153
        
128 154
    </script>
155
    
129 156
</body>
130 157
</html>
131 158

  
b/ui/templates/list.html
1
{% load i18n %}
2

  
1 3
<div id="machinesview" class="list">
2 4
    <div id="spinner"></div>
3 5
    <div class="actions">
......
150 152
// destroy action
151 153
$("a.enabled#action-destroy").live('click', function() {
152 154
	var checked = $("table.list-machines tbody input[type='checkbox']:checked");
155
	var serverIDs = [], serverNames=[];
153 156
	checked.each(function(i,c) {
154
		$.ajax({
155
			url: '/api/v1.0/servers/' + c.id.replace('input-',''),
156
			type: "DELETE",
157
			dataType: "json",
158
			success: function() {}
159
		});
160
		console.warn('destroying ' + c.id.replace('input-',''))
157
		serverID=c.id.replace('input-','');
158
		serverIDs.push(serverID);
159
		serverNames.push($('tr#'+serverID+' span.name').text());
161 160
	});
161
	confirm_action('destroy', destroy, serverIDs, serverNames);
162 162
	return false;
163 163
});
164 164

  
165 165

  
166 166
$("a.enabled#action-reboot").live('click', function() {
167
    // reboot action
168
    var payload = {
169
        "reboot": {"type" : "HARD"}
170
    };   
171

  
172
	var checked = $("table.list-machines tbody input[type='checkbox']:checked");	
167
	var checked = $("table.list-machines tbody input[type='checkbox']:checked");
168
	var serverIDs = [], serverNames=[];
173 169
	checked.each(function(i,c) {
174
		$.ajax({
175
			url: '/api/v1.0/servers/' + c.id.replace('input-','') + '/action',
176
			type: "POST",
177
			dataType: "json",
178
            data: JSON.stringify(payload),
179
			success: function() {}
180
		});
181
		console.warn('rebooting ' + c.id.replace('input-',''))
170
		serverID=c.id.replace('input-','');
171
		serverIDs.push(serverID);
172
		serverNames.push($('tr#'+serverID+' span.name').text());
182 173
	});
174
	confirm_action('reboot', reboot, serverIDs, serverNames);
183 175
	return false;
184 176
});
185 177

  
186 178

  
187 179
$("a.enabled#action-start").live('click', function() {
188
    // start action
189
    var payload = {
190
        "start": {"type" : "NORMAL"}
191
    };   
192

  
193
	var checked = $("tbody input[type='checkbox']:checked");	
180
	var checked = $("table.list-machines tbody input[type='checkbox']:checked");
181
	var serverIDs = [], serverNames=[];
194 182
	checked.each(function(i,c) {
195
		$.ajax({
196
			url: '/api/v1.0/servers/' + c.id.replace('input-','') + '/action',
197
			type: "POST",
198
            dataType: "json",
199
            data: JSON.stringify(payload),
200
			success: function() {}
201
		});
202
		console.warn('starting ' + c.id.replace('input-',''))		
183
		serverID=c.id.replace('input-','');
184
		serverIDs.push(serverID);
185
		serverNames.push($('tr#'+serverID+' span.name').text());
203 186
	});
187
	confirm_action('start', start, serverIDs, serverNames);
204 188
	return false;
205 189
});
206 190

  
207 191

  
208 192
$("a.enabled#action-shutdown").live('click', function() {
209
    // shutdown action. Not implemented by rackspace API atm
210
    var payload = {
211
        "shutdown": {"timeout" : "5"}
212
    };   
213

  
214
	var checked = $("tbody input[type='checkbox']:checked");	
193
	var checked = $("table.list-machines tbody input[type='checkbox']:checked");
194
	var serverIDs = [], serverNames=[];
215 195
	checked.each(function(i,c) {
216
		$.ajax({
217
			url: '/api/v1.0/servers/' + c.id.replace('input-','') + '/action',
218
			type: "POST",			
219
			dataType: "json",
220
            data: JSON.stringify(payload),
221
			success: function() {}
222
		});
223
		console.warn('shutting down ' + c.id.replace('input-',''))
196
		serverID=c.id.replace('input-','');
197
		serverIDs.push(serverID);
198
		serverNames.push($('tr#'+serverID+' span.name').text());
224 199
	});
200
	confirm_action('shutdown', shutdown, serverIDs, serverNames);
225 201
	return false;
226 202
});
227 203

  
204
update_vms();
205

  
228 206
</script>
b/ui/templates/machines.html
7 7

  
8 8
<!-- changing between standard/list view -->
9 9
<div id="view-select">
10
    <a id="standard" class="current" href="/machines">#</a>
10
    <a id="standard" href="/machines/standard">#</a>
11 11
    <span class="view-seperator">|</span>
12 12
    <a id="list" href="/machines/list">=</a>
13 13
</div>
14 14

  
15
<!-- the standard view -->
16
<div id="machinesview" class="standard">
17
    <div id="spinner"></div>
18
    <div class="machine" id="machine-template" style="display:none">
19
        <div class="state">
20
            <div class="status">{% trans "Running" %}</div>
21
            <div class="indicator"></div>
22
            <div class="indicator"></div>
23
            <div class="indicator"></div>
24
            <div class="indicator"></div>
25
        </div>
26
        <img class="logo" src="" />
27
        <a href="#" class="name">
28
            <h5>Νame: <span class="name">node.name</span><span class="rename"></span></h5>
29
        </a>
30
        <a href="#" class="ip">
31
            <h5>IP: <span class="public">node.public_ip</span></h5>
32
        </a>
33
        <h5 class="settings">
34
            {% trans "Show:" %} <a href="#">{% trans "disks" %}</a> | <a href="#">{% trans "networks" %}</a> | <a href="#">{% trans "group" %}</a>
35
        </h5>
36
        <div class="actions">
37
            <a href="#" class="action-start">{% trans "Start" %}</a>
38
            <a href="#" class="action-reboot">{% trans "Reboot" %}</a>
39
            <a href="#" class="action-shutdown">{% trans "Shutdown" %}</a>
40
            <a href="#" class="more">{% trans "more &hellip;" %}</a>
41
        </div>
42
        <div class="seperator"></div>
43
    </div>
44

  
45
    <div class="running"></div>
46
    <div id="mini" class="seperator"></div>
47
    <div class="terminated"></div>
48
</div>
49

  
50
<div id="machines" class="seperator"></div>
51

  
52 15
<!-- the form -->
53 16
<form action="#">
54 17
	<!-- scrollable root element -->
......
201 164
	<button>{% trans "No" %}</button>
202 165
</div>
203 166

  
167
<div id="machinesview"></div>
168

  
204 169
<script type="text/javascript"> 
205 170
var TIMEOUT = {{timeout}};
206 171
</script>
207 172

  
208 173
<script>
174

  
175

  
176
    
209 177
// hardcoded image types
210 178
var image_tags = {
211 179
                1: 'archlinux',
......
223 191
                20: 'ubuntu',
224 192
               };
225 193

  
226
// ajax error checking  
227
function ajax_error(jqXHR) {; 
228
    // prepare the error message
229
    $("#error-success h3").text('{% trans "Error!" %}');
230
    // check the error code
231
    switch (jqXHR.status) {
232
        case 400: // YY error/message
233
            $("#error-success p").text('{% trans "A Bad Request has been made." %}');
234
            break;
235
        case 404: // YY error/message
236
            $("#error-success p").text('{% trans "Your request has failed." %}');
237
            break;
238
        case 501: // XX error/message
239
            $("#error-success p").text('{% trans "There has been an Internal Error. Our administrators have been notified." %}');
240
            break;
241
        case 503: // XX error/message
242
            $("#error-success p").text('{% trans "This service is unavailble right now, please try again later." %}');
243
            break;
244
        default: // XXYY error/message
245
            $("#error-success p").text('{% trans "An Error has happened. Our administrators have been notified." %}');
246
    }
247
    // bring up error notification
248
    var triggers = $("a#notification").overlay({
249
	    // some mask tweaks suitable for modal dialogs
250
	    mask: {
251
		    color: '#ebecff',
252
		    opacity: '0.9'
253
	    },
254
        top: 'center',
255
	    closeOnClick: false,
256
        oneInstance: false,
257
        load: true,
258
        onClose: function(){
259
            $("div.pane#machines-pane").load($("a#standard").attr("href"));
260
        }
261
    });
262
    return false;
263
}
264

  
265
// ajax success checking
266
function ajax_success() {
267
    // prepare the error message
268
    $("#error-success h3").text('{% trans "Success!" %}');
269
    $("#error-success p").text('{% trans "Your request has been succefully executed." %}');
270
    // bring up success notification
271
    var triggers = $("a#notification").overlay({
272
	    // some mask tweaks suitable for modal dialogs
273
	    mask: {
274
		    color: '#ebecff',
275
		    opacity: '0.9'
276
	    },
277
        top: 'center',
278
	    closeOnClick: false,
279
        oneInstance: false,
280
        load: true,
281
        onClose: function(){
282
            $("div.pane#machines-pane").load($("a#standard").attr("href"));
283
        }
284
    });
285
    return false;
286
}
287

  
288
// confirmation overlay generation
289
function confirm_action(action_string, action_function, serverID, serverName) {
290
    $("#yes-no h3").text('You are about to ' + action_string + ' vm ' + serverName);
291
    // action confirmation overlay
292
    var triggers = $("a#confirmation").overlay({
293
	    // some mask tweaks suitable for modal dialogs
294
	    mask: {
295
		    color: '#ebecff',
296
		    opacity: '0.9'
297
	    },
298
        top: 'center',
299
        load: true
300
    });
301
    // yes or no?
302
    var buttons = $("#yes-no button").click(function(e) {
303
	    // get user input
304
	    var yes = buttons.index(this) === 0;
305
        //close the confirmation window
306
        $("a#confirmation").overlay().close(); 
307
        // return true=yes or false=no
308
        if (yes) {
309
            action_function(serverID);
310
        } else {
311
            // reload page
312
            $("div.pane#machines-pane").load($("a#standard").attr("href"));
313
        }
314
    });
315
    return false;
316
}
317

  
318
// get and show a list of running and terminated machines
319
function update_vms() {
320

  
321
    $(".running").text('');
322
    $(".terminated").text('');
323
    $("ul#standard-images").text('');
324
    $("ul#custom-images").text('');
325

  
326
    $.ajax({
327
        url: '/api/v1.0/servers/detail',
328
        type: "GET",
329
        timeout: TIMEOUT,
330
        dataType: "json",
331
        error: function(jqXHR, textStatus, errorThrown) { 
332
                    ajax_error(jqXHR);
333
                    return false;
334
                    },
335
        success: function(data, textStatus, jqXHR) {
336
            if ($(".running a.name").length + $(".terminated a.name").length == 0) {
337
            
338
                $.each(data.servers, function(i,server){
339
                    // if the machine is deleted it should not be included in any list
340
                    if (server.status == 'DELETED') {
341
                        return;
342
                    }
343
                    var machine = $("#machine-template").clone().attr("id", server.id).fadeIn("slow");
344
                    machine.find("input[type='checkbox']").attr("id", "input-" + server.id);
345
                    machine.find("input[type='checkbox']").attr("class", server.status);
346
                    machine.find("a.name span.name").text(server.name);
347
                    machine.find("img.logo").attr("src","static/machines/"+image_tags[server.imageId]+'.png');
348
                    machine.find("img.list-logo").attr("src","static/os_logos/"+image_tags[server.imageId]+'.png');
349
                    machine.find("img.list-logo").attr("title",image_tags[server.imageId]);
350
                    machine.find("span.imagetag").text(image_tags[server.imageId]);
351
    
352
                    machine.find("a.ip span.public").text(String(server.addresses.public.ip.addr).replace(',',' '));            
353
    
354
                    // TODO: handle SHARE_IP, SHARE_IP_NO_CONFIG, DELETE_IP, REBUILD, QUEUE_RESIZE, PREP_RESIZE, RESIZE, VERIFY_RESIZE, PASSWORD, RESCUE
355
                    if (server.status == 'BUILD'){
356
                        machine.find(".status").text('Building');
357
                        machine.appendTo(".running");
358
                    } else if (server.status == 'ACTIVE') {
359
                        machine.find(".status").text('Running');
360
                        machine.appendTo(".running"); 
361
                    } else if (server.status == 'REBOOT' || server.status == 'HARD_REBOOT') {
362
                        machine.find(".status").text('Rebooting');
363
                        machine.appendTo(".running");
364
                    } else if (server.status == 'STOPPED') {
365
                        machine.find(".status").text('Stopped');
366
                        machine.find("img.logo").attr("src","static/machines/"+image_tags[server.imageId]+'-off.png');
367
                        machine.find("img.list-logo").attr("src","static/os_logos/"+image_tags[server.imageId]+'-off.png');
368
                        machine.appendTo(".terminated");
369
                    } else if (server.status == 'ERROR') {
370
                        machine.find(".status").text('Error');
371
                        machine.find("img.logo").attr("src","static/machines/"+image_tags[server.imageId]+'-off.png');
372
                        machine.find("img.list-logo").attr("src","static/os_logos/"+image_tags[server.imageId]+'-off.png');
373
                        machine.appendTo(".terminated");
374
                    } 
375
                    else {
376
                        machine.find(".status").text('Unknown');
377
                        machine.find("img.logo").attr("src","static/machines/"+image_tags[server.imageId]+'-off.png');
378
                        machine.find("img.list-logo").attr("src","static/os_logos/"+image_tags[server.imageId]+'-off.png');
379
                        machine.appendTo(".terminated");
380
                    }
381
                });
382
            }
383
            $("#spinner").hide();
384
            $("div.machine:last-child").find("div.seperator").hide();
385
            // if the terminated list is populated then the seperator must be shown
386
            if ($(".terminated a.name").length > 0) {
387
                $("#mini.seperator").fadeIn("slow");
388
            }
389
            // creating the table in list view, if there are machines to show
390
            if ($("div.list table.list-machines tbody").length > 0) {
391
                $("div.list table.list-machines").dataTable({
392
                    "bInfo": false,
393
                    "bPaginate": false,
394
            		"bAutoWidth": false,
395
            		"bSort": true,    
396
                    "bStateSave": true,
397
                    //"sScrollY": "250px",
398
                    //"sScrollX": "500px",
399
                    //"sScrollXInner": "480px",
400
                    "aoColumnDefs": [
401
                        { "bSortable": false, "aTargets": [ 0 ] }
402
                    ]
403
                });
404
                $("div.list table.list-machines").show();
405
                $("div.list div.actions").show();
406
            }
407
        }
408
    });
409
    return false;
410
}
411

  
412
// get and show a list of anvailable standard and custom images
413
function update_images() { 
414
    $.ajax({
415
        url: '/api/v1.0/images/detail',
416
        type: "GET",
417
        //async: false,
418
        dataType: "json",
419
        timeout: TIMEOUT,
420
        error: function(jqXHR, textStatus, errorThrown) { 
421
                    ajax_error(jqXHR);
422
                    },
423
        success: function(data, textStatus, jqXHR) {
424
            if ($("ul#standard-images li").toArray().length + $("ul#custom-images li").toArray().length == 0) {
425
                $.each(data.images, function(i,image){
426
                    var img = $('#image-template').clone().attr("id","img-"+image.id).fadeIn("slow");
427
                    img.find("label").attr('for',"img-radio-" + image.id);
428
                    img.find(".image-title").text(image.name);
429
                    img.find(".description").text(image.description);
430
                    img.find(".size").text(image.size);
431
                    img.find("input.radio").attr('id',"img-radio-" + image.id);
432
                    if (i==0) img.find("input.radio").attr("checked","checked"); 
433
                    img.find("img.image-logo").attr('src','static/os_logos/'+image_tags[image.id]+'.png');
434
                    if (image.serverId) {
435
                        img.appendTo("ul#custom-images");
436
                    } else {
437
                        img.appendTo("ul#standard-images");
438
                    }
439
                });
440
            }
441
        }
442
    });
443
    return false;
444
}
445

  
446
var flavors = {}, disks = [], cpus = [], ram = [];
447

  
448
Array.prototype.unique = function () {
449
	var r = new Array();
450
	o:for(var i = 0, n = this.length; i < n; i++)
451
	{
452
		for(var x = 0, y = r.length; x < y; x++)
453
		{
454
			if(r[x]==this[i])
455
			{
456
				continue o;
457
			}
458
		}
459
		r[r.length] = this[i];
460
	}
461
	return r;
462
}
463

  
464
// get and configure flavor selection
465
function update_flavors() { 
466
    $.ajax({
467
        url: '/api/v1.0/flavors/detail',
468
        type: "GET",
469
        //async: false,
470
        dataType: "json",
471
        timeout: TIMEOUT,
472
        error: function(jqXHR, textStatus, errorThrown) { 
473
            ajax_error(jqXHR);
474
        },
475
        success: function(data, textStatus, jqXHR) {
476
            flavors = data.flavors;
477
            $.each(flavors, function(i, flavor) {
478
                cpus[i] = flavor['cpu'];
479
                disks[i] = flavor['disk'];
480
                ram[i] = flavor['ram'];
481
            });
482
            cpus = cpus.unique();
483
            disks = disks.unique();
484
            ram = ram.unique();
485
            // sliders for selecting VM flavor
486
            $("#cpu:range").rangeinput({min:0,
487
                                       value:0,
488
                                       step:1,
489
                                       progress: true,
490
                                       max:cpus.length-1});
491
            
492
            $("#storage:range").rangeinput({min:0,
493
                                       value:0,
494
                                       step:1,
495
                                       progress: true,
496
                                       max:disks.length-1});
497

  
498
            $("#ram:range").rangeinput({min:0,
499
                                       value:0,
500
                                       step:1,
501
                                       progress: true,
502
                                       max:ram.length-1});
503
            $("#small").click();
504
            
505
            // update the indicators when sliding
506
            $("#cpu:range").data().rangeinput.onSlide(function(event,value){
507
                $("#cpu-indicator")[0].value = cpus[Number(value)];
508
                $("#custom").click();
509
            });
510
            $("#ram:range").data().rangeinput.onSlide(function(event,value){
511
                $("#ram-indicator")[0].value = ram[Number(value)];
512
                $("#custom").click();
513
            });
514
            $("#storage:range").data().rangeinput.onSlide(function(event,value){
515
                $("#storage-indicator")[0].value = disks[Number(value)];
516
                $("#custom").click();
517
            });
518
        }
519
    });
520
    return false;
521
}
522
// return flavorId from cpu, disk, ram values
523
function identify_flavor(cpu, disk, ram){
524
    for (i=0;i<flavors.length;i++){
525
        if (flavors[i]['cpu'] == cpu && flavors[i]['disk']==disk && flavors[i]['ram']==ram) {
526
            return flavors[i]['id']
527
        }
528
    }
529
    return 0;
530
}
531

  
532 194
// switch to list view
533
$("#list").click(function(){
534
    $.cookie("list", '1'); // set list cookie
535
    $("div.standard#machinesview").load($("#list").attr("href"));
536
    $("a#standard")[0].className += ' activelink'
537
    this.style.color = '#5f8dd3';
538
    update_vms();
539
    return false;
540
});
541

  
195
$("a#list").click(function(){list_view(); return false;});
542 196
// switch to standard view
543
$("a#standard").click(function(){
544
    $.cookie("list", '0');
545
    href=$("a#standard").attr("href");
546
    $("div.pane#machines-pane").load(href);
547
    return false;
548
});
197
$("a#standard").click(function(){standard_view(); return false;});
549 198

  
550
// redirect to list view if the list cookie is set
551
if ($.cookie("list") == '1') {
552
    $("#list").click();
553
} else {
554
    // execute the update function to populate the list
555
    update_vms();
556
}
557 199

  
558 200
// launch VM creation wizard
559 201
$("a#create").click(function(){
......
563 205
    update_flavors(); 
564 206
    // launch the wizard
565 207
    $("#wizard").scrollable().begin();
208
    $("#wizard").show();
209
    $('a#create').data('overlay').load()    
566 210
});
567 211

  
568 212
// create wizard overlay
......
572 216
        effect: 'default', 
573 217
        top: '5%', 
574 218
        oneInstance: false,
575
        closeOnClick: false
219
        closeOnClick: false,
576 220
    });
577 221
});
578 222

  
579 223
// wizard
580 224
$(function() {
581 225
    var root = $("#wizard").scrollable();
582

  
583
    // some variables that we need
584 226
    var api = root.scrollable();
585

  
586 227
    // rangeinput with default configuration
587 228
    // validation logic is done inside the onBeforeSeek callback
588 229
    api.onBeforeSeek(function(event, i) {
......
607 248
	    }
608 249
	    // update status bar
609 250
	    $("#status li").removeClass("active").eq(i).addClass("active");
610
        
611 251
        // update confirm step
612 252
        if (api.getIndex()==0) {
613 253
            var image = $("input[type=radio][name=image-id]:checked");
......
617 257
                $("#machine_image-label")[0].textContent = imageName;
618 258
                $("input[type=text][name=machine_name]")[0].value = "My " + imageName + " server";
619 259
            }
260
        } else if (api.getIndex()==1) {
620 261
            $("#machine_cpu-label")[0].textContent = $("#cpu-indicator")[0].value;
621 262
            $("#machine_ram-label")[0].textContent = $("#ram-indicator")[0].value;
622 263
            $("#machine_storage-label")[0].textContent = $("#storage-indicator")[0].value;
......
701 342
        }
702 343
    };
703 344

  
345
    $('a#create').data('overlay').close();
704 346
    $.ajax({
705 347
    url: "/api/v1.0/servers",
706 348
    type: "POST",
......
723 365
    $("#wizard").hide();
724 366
});
725 367

  
726
// reboot action
727
function reboot(serverID){
728
    // ajax post reboot call
729
    var payload = {
730
        "reboot": {"type" : "HARD"}
731
    };   
732

  
733
    $.ajax({
734
        url: '/api/v1.0/servers/' + serverID + '/action',
735
        type: "POST",        
736
        dataType: "json",
737
        data: JSON.stringify(payload),
738
        timeout: TIMEOUT,
739
        error: function(jqXHR, textStatus, errorThrown) { 
740
                    ajax_error(jqXHR);
741
                    },
742
        success: function(data, textStatus, jqXHR) {
743
                    if ( jqXHR.status == '202') {
744
                        ajax_success(jqXHR);
745
                    } else {
746
                        ajax_error(jqXHR);
747
                    }}
748
    });
749
    console.warn('rebooting ' + serverID);    
750
    return false;
751
}
752

  
753
// shutdown action
754
function shutdown(serverID) {
755
    // ajax post shutdown call
756
    var payload = {
757
        "shutdown": {"timeout" : "5"}
758
    };   
759

  
760
    $.ajax({
761
	    url: '/api/v1.0/servers/' + serverID + '/action',
762
	    type: "POST",
763
	    dataType: "json",
764
        data: JSON.stringify(payload),
765
        timeout: TIMEOUT,
766
        error: function(jqXHR, textStatus, errorThrown) { 
767
                    ajax_error(jqXHR);
768
                    },
769
        success: function(data, textStatus, jqXHR) {
770
                    if ( jqXHR.status == '202') {
771
                        ajax_success(jqXHR);
772
                    } else {
773
                        ajax_error(jqXHR);
774
                    }}             
775
    });
776
    console.warn('shutting down ' + serverID);        
777
    return false;    
778
}
779

  
780

  
781
// start action
782
function start(serverID){
783
    // ajax post start call
784
    var payload = {
785
        "start": {"type" : "NORMAL"}
786
    };   
787

  
788
    $.ajax({
789
        url: '/api/v1.0/servers/' + serverID + '/action',
790
        type: "POST",
791
        dataType: "json",
792
        data: JSON.stringify(payload),
793
        timeout: TIMEOUT,
794
        error: function(jqXHR, textStatus, errorThrown) { 
795
                    ajax_error(jqXHR);
796
                    },
797
        success: function(data, textStatus, jqXHR) {
798
                    if ( jqXHR.status == '202') {
799
                        ajax_success(jqXHR);
800
                    } else {
801
                        ajax_error(jqXHR);
802
                    }}
803
    });
804
    console.warn('starting ' + serverID);        
805
    return false;
806
}
807 368

  
808 369

  
809 370
// basic functions executed on page load
......
811 372
// create tabs for main menu
812 373
$("ul.tabs").tabs("div.panes ul");
813 374

  
814
// intercept reboot click 
815
$("div.actions a.action-reboot").live('click', function(){ 
816
    var serverID = $(this).parent().parent().attr("id");
817
    var serverName = $(this).parent().prevAll("a.name").find("span.name").text();
818
    confirm_action('reboot', reboot, serverID, serverName);
819
    return false;
820
});
821 375

  
822
// intercept shutdown click
823
$("div.actions a.action-shutdown").live('click', function(){ 
824
    var serverID = $(this).parent().parent().attr("id");
825
    var serverName = $(this).parent().prevAll("a.name").find("span.name").text();
826
    confirm_action('shutdown', shutdown, serverID, serverName);
827
    return false;
828
});
829
// intercept start click
830
$("div.actions a.action-start").live('click', function(){ 
831
    var serverID = $(this).parent().parent().attr("id");
832
    var serverName = $(this).parent().prevAll("a.name").find("span.name").text();
833
    confirm_action('start', start, serverID, serverName);
834
    return false;
835
});
376

  
836 377
</script>
b/ui/templates/standard.html
1
{% load i18n %}
2

  
3
<!-- the standard view -->
4
<div id="machinesview" class="standard">
5
    <div id="spinner"></div>
6
    <div class="machine" id="machine-template" style="display:none">
7
        <div class="state">
8
            <div class="status">{% trans "Running" %}</div>
9
            <div class="indicator"></div>
10
            <div class="indicator"></div>
11
            <div class="indicator"></div>
12
            <div class="indicator"></div>
13
        </div>
14
        <img class="logo" src="" />
15
        <a href="#" class="name">
16
            <h5>Name: <span class="name">node.name</span><span class="rename"></span></h5>
17
        </a>
18
        <a href="#" class="ip">
19
            <h5>IP: <span class="public">node.public_ip</span></h5>
20
        </a>
21
        <h5 class="settings">
22
            {% trans "Show:" %} <a href="#">{% trans "disks" %}</a> | <a href="#">{% trans "networks" %}</a> | <a href="#">{% trans "group" %}</a>
23
        </h5>
24
        <div class="actions">
25
            <a href="#" class="action-start">{% trans "Start" %}</a>
26
            <a href="#" class="action-reboot">{% trans "Reboot" %}</a>
27
            <a href="#" class="action-shutdown">{% trans "Shutdown" %}</a>
28
            <a href="#" class="more">{% trans "more &hellip;" %}</a>
29
        </div>
30
        <div class="seperator"></div>
31
    </div>
32

  
33
    <div class="running"></div>
34
    <div id="mini" class="seperator"></div>
35
    <div class="terminated"></div>
36
</div>
37

  
38
<div id="machines" class="seperator"></div>
39

  
40
<script>
41
// intercept reboot click 
42
$("div.actions a.action-reboot").live('click', function(){
43
    var serverID = $(this).parent().parent().attr("id");
44
    var serverName = $(this).parent().prevAll("a.name").find("span.name").text();
45
    confirm_action('reboot', reboot, [serverID], [serverName]);
46
    return false;
47
});
48

  
49
// intercept shutdown click
50
$("div.actions a.action-shutdown").live('click', function(){ 
51
    var serverID = $(this).parent().parent().attr("id");
52
    var serverName = $(this).parent().prevAll("a.name").find("span.name").text();
53
    confirm_action('shutdown', shutdown, [serverID], [serverName]);
54
    return false;
55
});
56
// intercept start click
57
$("div.actions a.action-start").live('click', function(){ 
58
    var serverID = $(this).parent().parent().attr("id");
59
    var serverName = $(this).parent().prevAll("a.name").find("span.name").text();
60
    confirm_action('start', start, [serverID], [serverName]);
61
    return false;
62
});
63

  
64
update_vms();
65

  
66
</script>
b/ui/urls.py
4 4
urlpatterns = patterns('',
5 5
    (r'^$', 'synnefo.ui.views.home'),
6 6
    (r'^machines$', 'synnefo.ui.views.machines'),
7
    (r'^machines/standard$', 'synnefo.ui.views.machines_standard'),    
7 8
    (r'^machines/list$', 'synnefo.ui.views.machines_list'),
8 9
    (r'^disks$', 'synnefo.ui.views.disks'),
9 10
    (r'^images$', 'synnefo.ui.views.images'),
b/ui/views.py
20 20
def machines(request):
21 21
    context = {}
22 22
    return template('machines', context)
23
   
23

  
24
def machines_standard(request):
25
    context = {}
26
    return template('standard', context)
27
    
24 28
def machines_list(request):
25 29
    context = {}
26 30
    return template('list', context)

Also available in: Unified diff