diff --git a/circle/dashboard/static/dashboard/dashboard.css b/circle/dashboard/static/dashboard/dashboard.css index 30c1448..e33bdbd 100644 --- a/circle/dashboard/static/dashboard/dashboard.css +++ b/circle/dashboard/static/dashboard/dashboard.css @@ -321,7 +321,7 @@ a.hover-black { } .template-list-table-thin { - width: 95px; + width: 100px; } @@ -540,3 +540,11 @@ footer a, footer a:hover, footer a:visited { bottom: 10px; right: 20px; } + +.vm-list-table th i { + margin-top: 3px; +} + +.table-sorting { + display: none; +} diff --git a/circle/dashboard/static/dashboard/js/stupidtable.min.js b/circle/dashboard/static/dashboard/js/stupidtable.min.js new file mode 100644 index 0000000..76b0882 --- /dev/null +++ b/circle/dashboard/static/dashboard/js/stupidtable.min.js @@ -0,0 +1,119 @@ +// Stupid jQuery table plugin. +/* + * Source: https://github.com/joequery/Stupid-Table-Plugin + * The Stupid jQuery Plugin is licensed under the MIT license. + * Copyright (c) 2012 Joseph McCullough + */ + +// Call on a table +// sortFns: Sort functions for your datatypes. +(function($) { + + $.fn.stupidtable = function(sortFns) { + return this.each(function() { + var $table = $(this); + sortFns = sortFns || {}; + + // Merge sort functions with some default sort functions. + sortFns = $.extend({}, $.fn.stupidtable.default_sort_fns, sortFns); + + + // ==================================================== // + // Begin execution! // + // ==================================================== // + + // Do sorting when THs are clicked + $table.on("click.stupidtable", "th", function() { + var $this = $(this); + var th_index = 0; + var dir = $.fn.stupidtable.dir; + + $table.find("th").slice(0, $this.index()).each(function() { + var cols = $(this).attr("colspan") || 1; + th_index += parseInt(cols,10); + }); + + // Determine (and/or reverse) sorting direction, default `asc` + var sort_dir = $this.data("sort-default") || dir.ASC; + if ($this.data("sort-dir")) + sort_dir = $this.data("sort-dir") === dir.ASC ? dir.DESC : dir.ASC; + + // Choose appropriate sorting function. + var type = $this.data("sort") || null; + + // Prevent sorting if no type defined + if (type === null) { + return; + } + + // Trigger `beforetablesort` event that calling scripts can hook into; + // pass parameters for sorted column index and sorting direction + $table.trigger("beforetablesort", {column: th_index, direction: sort_dir}); + // More reliable method of forcing a redraw + $table.css("display"); + + // Run sorting asynchronously on a timout to force browser redraw after + // `beforetablesort` callback. Also avoids locking up the browser too much. + setTimeout(function() { + // Gather the elements for this column + var column = []; + var sortMethod = sortFns[type]; + var trs = $table.children("tbody").children("tr"); + + // Extract the data for the column that needs to be sorted and pair it up + // with the TR itself into a tuple + trs.each(function(index,tr) { + var $e = $(tr).children().eq(th_index); + var sort_val = $e.data("sort-value"); + var order_by = typeof(sort_val) !== "undefined" ? sort_val : $e.text(); + column.push([order_by, tr]); + }); + + // Sort by the data-order-by value + column.sort(function(a, b) { return sortMethod(a[0], b[0]); }); + if (sort_dir != dir.ASC) + column.reverse(); + + // Replace the content of tbody with the sorted rows. Strangely (and + // conveniently!) enough, .append accomplishes this for us. + trs = $.map(column, function(kv) { return kv[1]; }); + $table.children("tbody").append(trs); + + // Reset siblings + $table.find("th").data("sort-dir", null).removeClass("sorting-desc sorting-asc"); + $this.data("sort-dir", sort_dir).addClass("sorting-"+sort_dir); + + // Trigger `aftertablesort` event. Similar to `beforetablesort` + $table.trigger("aftertablesort", {column: th_index, direction: sort_dir}); + // More reliable method of forcing a redraw + $table.css("display"); + }, 10); + }); + }); + }; + + // Enum containing sorting directions + $.fn.stupidtable.dir = {ASC: "asc", DESC: "desc"}; + + $.fn.stupidtable.default_sort_fns = { + "int": function(a, b) { + return parseInt(a, 10) - parseInt(b, 10); + }, + "float": function(a, b) { + return parseFloat(a) - parseFloat(b); + }, + "string": function(a, b) { + if (a < b) return -1; + if (a > b) return +1; + return 0; + }, + "string-ins": function(a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + if (a < b) return -1; + if (a > b) return +1; + return 0; + } + }; + +})(jQuery); diff --git a/circle/dashboard/static/dashboard/template-list.js b/circle/dashboard/static/dashboard/template-list.js index 7750f3c..30f7fd8 100644 --- a/circle/dashboard/static/dashboard/template-list.js +++ b/circle/dashboard/static/dashboard/template-list.js @@ -20,6 +20,27 @@ $(function() { }); return false; }); + + /* template table sort */ + var ttable = $(".template-list-table").stupidtable(); + + ttable.on("beforetablesort", function(event, data) { + // pass + }); + + ttable.on("aftertablesort", function(event, data) { + $(".template-list-table thead th i").remove(); + + var icon_html = ''; + $(".template-list-table thead th").eq(data.column).append(icon_html); + }); + + // only if js is enabled + $(".template-list-table thead th").css("cursor", "pointer"); + + $(".template-list-table th a").on("click", function(event) { + event.preventDefault(); + }); }); diff --git a/circle/dashboard/static/dashboard/vm-list.js b/circle/dashboard/static/dashboard/vm-list.js index f699a45..4cefcf8 100644 --- a/circle/dashboard/static/dashboard/vm-list.js +++ b/circle/dashboard/static/dashboard/vm-list.js @@ -17,14 +17,14 @@ $(function() { if (ctrlDown) { setRowColor($(this)); if(!$(this).hasClass('vm-list-selected')) { - selected.splice(selected.indexOf($(this).index()), 1); + selected.splice(getSelectedIndex($(this).index()), 1); } else { - selected.push($(this).index()); + selected.push({'index': $(this).index(), 'vm': $(this).data("vm-pk")}); } retval = false; } else if(shiftDown) { if(selected.length > 0) { - start = selected[selected.length - 1] + 1; + start = selected[selected.length - 1]['index'] + 1; end = $(this).index(); if(start > end) { @@ -32,8 +32,9 @@ $(function() { } for(var i = start; i <= end; i++) { - if(selected.indexOf(i) < 0) { - selected.push(i); + var vm = $(".vm-list-table tbody tr").eq(i).data("vm-pk"); + if(!isAlreadySelected(vm)) { + selected.push({'index': i, 'vm': vm}); setRowColor($('.vm-list-table tbody tr').eq(i)); } } @@ -42,13 +43,13 @@ $(function() { } else { $('.vm-list-selected').removeClass('vm-list-selected'); $(this).addClass('vm-list-selected'); - selected = [$(this).index()]; + selected = [{'index': $(this).index(), 'vm': $(this).data("vm-pk")}]; } // reset btn disables $('.vm-list-table tbody tr .btn').attr('disabled', false); // show/hide group controls - if(selected.length > 1) { + if(selected.length > 0) { $('.vm-list-group-control a').attr('disabled', false); for(var i = 0; i < selected.length; i++) { $('.vm-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true); @@ -61,7 +62,7 @@ $(function() { $('#vm-list-group-migrate').click(function() { - console.log(collectIds(selected)); + // pass? }); $('.vm-list-details').popover({ @@ -131,8 +132,9 @@ $(function() { $('#vm-list-group-select-all').click(function() { $('.vm-list-table tbody tr').each(function() { var index = $(this).index(); - if(selected.indexOf(index) < 0) { - selected.push(index); + var vm = $(this).data("vm-pk"); + if(!isAlreadySelected(vm)) { + selected.push({'index': index, 'vm': vm}); $(this).addClass('vm-list-selected'); } }); @@ -154,13 +156,51 @@ $(function() { ); return false; }); + + /* table sort */ + var table = $(".vm-list-table").stupidtable(); + + table.on("beforetablesort", function(event, data) { + $(".table-sorting").show(); + }); + + table.on("aftertablesort", function(event, data) { + // this didn't work ;; + // var th = $("this").find("th"); + $(".table-sorting").hide(); + + $(".vm-list-table thead th i").remove(); + + var icon_html = ''; + $(".vm-list-table thead th").eq(data.column).append(icon_html); + }); + + // only if js is enabled + $(".vm-list-table thead th").css("cursor", "pointer"); + + $(".vm-list-table th a").on("click", function(event) { + event.preventDefault(); + }); }); +function isAlreadySelected(vm) { + for(var i=0; i + + {% endblock %} diff --git a/circle/dashboard/templates/dashboard/vm-list.html b/circle/dashboard/templates/dashboard/vm-list.html index f36b838..983f0df 100644 --- a/circle/dashboard/templates/dashboard/vm-list.html +++ b/circle/dashboard/templates/dashboard/vm-list.html @@ -9,6 +9,10 @@
+
+ {% trans "Sorting ... " %} + +

{% trans "Virtual machines" %}

@@ -32,22 +36,39 @@
- - - - - {% if user.is_superuser %}{% endif %} + + + + + {% if user.is_superuser %}{% endif %} {% for i in object_list %} - + - {% if user.is_superuser %}{% endif %} + {% if user.is_superuser %} + + {% endif %} {% empty %} - + {% endfor %}
{% trans "ID" %}{% trans "Name" %}{% trans "State" %}{% trans "Owner" %}{% trans "Node" %} + {% trans "ID" as t %} + {% include "dashboard/vm-list/header-link.html" with name=t sort="pk" %} + + {% trans "Name" as t %} + {% include "dashboard/vm-list/header-link.html" with name=t sort="name" %} + + {% trans "State" as t %} + {% include "dashboard/vm-list/header-link.html" with name=t sort="status" %} + + {% trans "Owner" as t %} + {% include "dashboard/vm-list/header-link.html" with name=t sort="owner" %} + + {% trans "Node" as t %} + {% include "dashboard/vm-list/header-link.html" with name=t sort="node" %} +
{{i.pk}}
{{ i.name }} {{ i.get_status_display }} {{ i.owner }}{{ i.node.name|default:"-" }}{{ i.node.name|default:"-" }}
{% trans "You have no virtual machines." %}
{% trans "You have no virtual machines." %}
@@ -92,4 +113,5 @@ {% block extra_js %} + {% endblock %} diff --git a/circle/dashboard/templates/dashboard/vm-list/header-link.html b/circle/dashboard/templates/dashboard/vm-list/header-link.html new file mode 100644 index 0000000..79d10c5 --- /dev/null +++ b/circle/dashboard/templates/dashboard/vm-list/header-link.html @@ -0,0 +1 @@ +{{ name }} diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py index 0e96058..b793e50 100644 --- a/circle/dashboard/views.py +++ b/circle/dashboard/views.py @@ -1053,7 +1053,8 @@ class TemplateList(LoginRequiredMixin, SingleTableView): def get_context_data(self, *args, **kwargs): context = super(TemplateList, self).get_context_data(*args, **kwargs) - context['lease_table'] = LeaseListTable(Lease.objects.all()) + context['lease_table'] = LeaseListTable(Lease.objects.all(), + request=self.request) return context def get_queryset(self): @@ -1126,6 +1127,14 @@ class VmList(LoginRequiredMixin, ListView): s = self.request.GET.get("s") if s: queryset = queryset.filter(name__icontains=s) + + sort = self.request.GET.get("sort") + # remove "-" that means descending order + # also check if the column name is valid + if (sort and + (sort[1:] if sort[0] == "-" else sort) + in [i.name for i in Instance._meta.fields] + ["pk"]): + queryset = queryset.order_by(sort) return queryset.select_related('owner', 'node')