1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

feat(tables): tables sort with tablesort, the grouped VM list within each host

sortable.js 0.8.0 (abandoned since 2014) sorted only a table's first
body: in the grouped VM list the first host's row moved among its VMs
and the other hosts never sorted, and memory sorted as text ("1024 MB"
before "512 MB"). tablesort 5.7.1 sorts each table body on its own and
keeps a row marked data-sort-method="none" in place, so hosts stay in
name order with their row on top and their VMs sort below it. Memory,
vCPUs and volume sizes sort by their numbers (data-sort). Headers are
reachable by keyboard and carry aria-sort.

base.html loads tablesort for every table marked data-sortable; the
per-page sortable includes and sortable-theme-bootstrap.css go, so these
tables take the theme's table style like the others. The grouped list's
row numbers go (meaningless once sorted). The VM owners and VM logs
tables were marked sortable but never loaded the script; they stay
unsorted.
This commit is contained in:
catborise 2026-10-24 18:15:04 +03:00
parent ee72e36ad1
commit 7cf12b997b
20 changed files with 84 additions and 180 deletions

View file

@ -4,9 +4,6 @@
{% load operations %}
{% load bootstrap_icons %}
{% block title %}{% trans "Instances" %} - {{ compute.name }}{% endblock %}
{% block style %}
<link rel="stylesheet" href="{% static "css/sortable-theme-bootstrap.css" %}" />
{% endblock %}
{% block page_heading %}{{ compute.name }} - {% trans "Instances" %}{% endblock page_heading %}
{% block page_heading_extra %}
@ -67,15 +64,15 @@
</div>
{% else %}
<div class="table-responsive">
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
<table class="table table-hover" data-sortable>
<thead>
<tr>
<th scope="col">{% trans 'Name' %}<br>{% trans 'Description' %}</th>
<th scope="col">{% trans 'User' %}</th>
<th scope="col">{% trans 'Status' %}</th>
<th scope="col">{% trans 'VCPU' %}</th>
<th scope="col" class="text-end">{% trans 'Memory' %}</th>
<th scope="col" data-sortable="false">{% trans 'Actions' %}</th>
<th scope="col" data-sort-method="number">{% trans 'VCPU' %}</th>
<th scope="col" class="text-end" data-sort-method="number">{% trans 'Memory' %}</th>
<th scope="col" data-sort-method="none">{% trans 'Actions' %}</th>
</tr>
</thead>
<tbody class="searchable">
@ -99,8 +96,8 @@
<td>
{% include 'instance_state.html' %}
</td>
<td>{{ instance.info.3 }}</td>
<td><p class="text-end">{% if instance.info %}{% widthratio instance.info.1 1024 1 %} MiB{% endif %}</p></td>
<td data-sort="{{ instance.info.3 }}">{{ instance.info.3 }}</td>
<td data-sort="{{ instance.info.1 }}"><p class="text-end">{% if instance.info %}{% widthratio instance.info.1 1024 1 %} MiB{% endif %}</p></td>
<td class="text-nowrap">
{% include 'instance_actions.html' %}
</td>
@ -116,6 +113,5 @@
{% block script %}
<script src="{% static 'js/poll.js' %}"></script>
{% job_panel_script job_resource %}
<script src="{% static "js/sortable.min.js" %}"></script>
<script src="{% static 'js/filter-table.js' %}"></script>
{% endblock %}

View file

@ -22,13 +22,13 @@
{% else %}
<div class="col-lg-12">
<div class="table-responsive">
<table class="table table-striped table-hover sortable-theme-bootstrap" data-sortable>
<table class="table table-striped table-hover" data-sortable>
<thead>
<tr>
<th span="col" class="col-sm-3">{% trans "Name" %}</th>
<th span="col" class="col-sm-2">{% trans "Status" %}</th>
<th span="col" class="col-sm-5">{% trans "Details" %}</th>
<th span="col" class="col-sm-2 text-center">{% trans "Actions" %}</th>
<th span="col" class="col-sm-2 text-center" data-sort-method="none">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody class="searchable">
@ -67,6 +67,5 @@
{% endblock content %}
{% block script %}
<script src="{% static 'js/sortable.min.js' %}"></script>
<script src="{% static 'js/filter-table.js' %}"></script>
{% endblock script %}

View file

@ -2,9 +2,6 @@
{% load i18n %}
{% load static %}
{% block title %}{% trans "Instances" %}{% endblock %}
{% block style %}
<link rel="stylesheet" href="{% static 'css/sortable-theme-bootstrap.css' %}" />
{% endblock %}
{% block page_heading %}{% trans "Instances" %}{% endblock page_heading %}
{% block page_heading_extra %}
@ -47,7 +44,6 @@
</div>
{% endblock content %}
{% block script %}
<script src="{% static 'js/sortable.min.js' %}"></script>
<script src="{% static 'js/instance_filter.js' %}"></script>
{% if request.user.is_superuser %}
<script>

View file

@ -1,25 +1,25 @@
{% load i18n %}
{% load bootstrap_icons %}
<div class="table-responsive">
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
<table class="table table-hover" data-sortable>
<thead>
<tr>
<th scope="col" data-sortable="false"><a class="link-primary" href="#" id="hide-all-instances" onclick="hide_all_host_instances()">#</a></th>
<th scope="col" data-sort-method="none"><a class="link-primary" href="#" id="hide-all-instances" onclick="hide_all_host_instances()">#</a></th>
<th scope="col">{% trans "Name" %}<br>{% trans "Description" %}</th>
<th scope="col" class="d-none d-sm-table-cell">{% trans "User"%}</th>
<th scope="col">{% trans "Status" %}</th>
{% if app_settings.VM_DRBD_STATUS == 'True' %}
<th scope="col">{% trans "Role/Disk" %}</th>
{% endif %}
<th scope="col" class="d-none d-sm-table-cell">{% trans "VCPU" %}</th>
<th scope="col" class="d-none d-sm-table-cell">{% trans "Memory" %}</th>
<th scope="col" style="width:200px;" data-sortable="false">{% trans "Actions" %} & {% trans "Mem Usage" %}</th>
<th scope="col" class="d-none d-sm-table-cell" data-sort-method="number">{% trans "VCPU" %}</th>
<th scope="col" class="d-none d-sm-table-cell" data-sort-method="number">{% trans "Memory" %}</th>
<th scope="col" style="width:200px;" data-sort-method="none">{% trans "Actions" %} & {% trans "Mem Usage" %}</th>
</tr>
</thead>
{% for compute in computes %}
<tbody class="searchable" data-host="{{ compute.name }}">
{% if compute.instance_set.count > 0 %}
<tr class="table-secondary fw-bold border-bottom border-dark">
<tr class="table-secondary fw-bold border-bottom border-dark" data-sort-method="none">
<td>
<span id="collapse_host_instances_{{ compute.id }}" onclick="hide_host_instances('{{ compute.id }}');" class="bi bi-chevron-up" ></span>
</td>
@ -56,7 +56,7 @@
</tr>
{% for instance in compute.instance_set.all %}
<tr host="{{ compute.id }}">
<td class="text-right">{{ forloop.counter }} </td>
<td></td>
<td>
<a class="link-primary" href="{% url 'instances:instance' instance.id %}">{{ instance.name }}</a>
<br>
@ -80,8 +80,8 @@
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="badge bg-success-subtle text-success-emphasis">{% else %}<span class="badge bg-danger-subtle text-danger-emphasis">{% endif %}{{ instance.drbd }}</span>
</td>
{% endif %}
<td>{{ instance.info.3 }}</td>
<td>{% if instance.info %}{% widthratio instance.info.2 1024 1 %} MB{% endif %}</td>
<td data-sort="{{ instance.info.3 }}">{{ instance.info.3 }}</td>
<td data-sort="{{ instance.info.2 }}">{% if instance.info %}{% widthratio instance.info.2 1024 1 %} MB{% endif %}</td>
<td class="text-nowrap">
{% include 'instance_actions.html' %}
</td>
@ -89,8 +89,6 @@
{% endfor %}
{% endif %}
</tbody>
{% empty %}
<tbody></tbody>{# sortable.js needs a table body #}
{% endfor %}
</table>
</div>

View file

@ -1,6 +1,6 @@
{% load i18n %}
<div class="table-responsive">
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
<table class="table table-hover" data-sortable>
<thead>
<tr>
<th scope="col">{% trans 'Name' %}<br>{% trans 'Description' %}</th>
@ -11,9 +11,9 @@
{% if app_settings.VM_DRBD_STATUS == 'True' %}
<th scope="col">{% trans 'Role/Disk' %}</th>
{% endif %}
<th scope="col">{% trans 'VCPU' %}</th>
<th scope="col">{% trans 'Memory' %}</th>
<th scope="col" data-sortable="false">{% trans 'Actions' %}</th>
<th scope="col" data-sort-method="number">{% trans 'VCPU' %}</th>
<th scope="col" data-sort-method="number">{% trans 'Memory' %}</th>
<th scope="col" data-sort-method="none">{% trans 'Actions' %}</th>
</tr>
</thead>
<tbody class="searchable">
@ -47,8 +47,8 @@
{% if instance.drbd == "Primary/OK" or instance.drbd == "Secondary/OK" %}<span class="badge bg-success-subtle text-success-emphasis">{% else %}<span class="badge bg-danger-subtle text-danger-emphasis">{% endif %}{{ instance.drbd }}</span>
</td>
{% endif %}
<td>{{ instance.info.3 }}</td>
<td>{% if instance.info %}{% widthratio instance.info.2 1024 1 %} MB{% endif %}</td>
<td data-sort="{{ instance.info.3 }}">{{ instance.info.3 }}</td>
<td data-sort="{{ instance.info.2 }}">{% if instance.info %}{% widthratio instance.info.2 1024 1 %} MB{% endif %}</td>
<td class="text-nowrap">
{% include 'instance_actions.html' %}
</td>

View file

@ -684,7 +684,7 @@
</div>
</div>
<div class="table-responsive">
<table class="table table-striped sortable-theme-bootstrap mt-3" data-sortable>
<table class="table table-striped mt-3">
<tbody class="searchable">
{% for userinstance in userinstances %}
<tr>

View file

@ -65,7 +65,7 @@
</div>
<div role="tabpanel" class="tab-pane tab-pane-bordered" id="logs">
<div class="table-responsive">
<table class="table table-striped sortable-theme-bootstrap" id="logs_table" data-sortable>
<table class="table table-striped" id="logs_table">
<thead>
<tr>
<th scope="col">{% trans "Date" %}</th>

View file

@ -2,13 +2,11 @@
from unittest.mock import PropertyMock, patch
from appsettings.models import AppSettings
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from instances.models import Instance
from lxml import html
class InstancesIndexTests(TestCase):
@ -44,10 +42,3 @@ class InstancesIndexTests(TestCase):
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "test4")
self.assertEqual(list(response.context["computes"][0].instance_set.all()), [])
def test_the_grouped_list_without_computes_has_a_table_body(self):
# sortable.js reads the first tbody's rows: without one it throws and leaves the table unsortable
AppSettings.objects.filter(key="VIEW_INSTANCES_LIST_STYLE").update(value="grouped")
self.client.force_login(get_user_model().objects.create_superuser("grouped-admin", "g@example.com", "pw"))
page = html.fromstring(self.client.get(reverse("instances:index")).content)
self.assertEqual(len(page.xpath("//table[@data-sortable]/tbody")), 1)

View file

@ -127,6 +127,10 @@ class StateTests(TestCase):
self.assertEqual(options, ["All states", *LABELS.values(), "Unknown"])
marks = page.xpath("//tbody[@data-host='st']//tr//@data-state")
self.assertEqual(marks, list(LABELS.values()))
# the host row stays on top of its VMs; memory and vCPUs sort as numbers
rows = page.xpath("//tbody[@data-host='st']/tr")
self.assertEqual(rows[0].get("data-sort-method"), "none")
self.assertEqual([r.xpath("td[@data-sort]/@data-sort") for r in rows[1:2]], [["2", "2097152"]])
class DetailStateTests(TestCase):

View file

@ -161,7 +161,6 @@
{% endblock %}
{% block script %}
<script src="{% static 'js/sortable.min.js' %}"></script>
<script src="{% static 'js/ace/ace.js' %}"></script>
<script>
var editor = ace.edit("edit_editor");

View file

@ -47,13 +47,13 @@
<div class="col-lg-12">
{% if nwfilters %}
<div class="table-responsive">
<table class="table sortable-theme-bootstrap" data-sortable>
<table class="table" data-sortable>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" data-sort-method="number">#</th>
<th scope="col">{% trans "UUID" %}</th>
<th scope="col">{% trans "Name" %}</th>
<th scope="colgroup" data-sortable="false" colspan="3">{% trans "Action" %}</th>
<th scope="colgroup" data-sort-method="none" colspan="3">{% trans "Action" %}</th>
</tr>
</thead>
<tbody class="searchable">
@ -140,7 +140,6 @@
{% endblock %}
{% block script %}
<script src="{% static 'js/sortable.min.js' %}"></script>
<script src="{% static 'js/filter-table.js' %}"></script>
<script src="{% static 'js/ace/ace.js' %}"></script>
<script>

View file

@ -1,109 +0,0 @@
/* line 2, ../sass/_sortable.sass */
table[data-sortable] {
border-collapse: collapse;
border-spacing: 0;
}
/* line 6, ../sass/_sortable.sass */
table[data-sortable] th {
vertical-align: bottom;
font-weight: bold;
}
/* line 10, ../sass/_sortable.sass */
table[data-sortable] th, table[data-sortable] td {
text-align: left;
padding: 10px;
}
/* line 14, ../sass/_sortable.sass */
table[data-sortable] th:not([data-sortable="false"]) {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-touch-callout: none;
cursor: pointer;
}
/* line 26, ../sass/_sortable.sass */
table[data-sortable] th:after {
content: "";
visibility: hidden;
display: inline-block;
vertical-align: inherit;
height: 0;
width: 0;
border-width: 5px;
border-style: solid;
border-color: transparent;
margin-right: 1px;
margin-left: 10px;
float: right;
}
/* line 40, ../sass/_sortable.sass */
table[data-sortable] th[data-sorted="true"]:after {
visibility: visible;
}
/* line 43, ../sass/_sortable.sass */
table[data-sortable] th[data-sorted-direction="descending"]:after {
border-top-color: inherit;
margin-top: 8px;
}
/* line 47, ../sass/_sortable.sass */
table[data-sortable] th[data-sorted-direction="ascending"]:after {
border-bottom-color: inherit;
margin-top: 3px;
}
/* line 6, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick {
color: #333333;
background: white;
border: 1px solid #e0e0e0;
}
/* line 11, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick thead th {
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #eeeeee));
background-image: -webkit-linear-gradient(#ffffff, #eeeeee);
background-image: -moz-linear-gradient(#ffffff, #eeeeee);
background-image: -o-linear-gradient(#ffffff, #eeeeee);
background-image: linear-gradient(#ffffff, #eeeeee);
background-color: #f0f0f0;
border-bottom: 1px solid #e0e0e0;
}
/* line 16, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick tbody td {
border-top: 1px solid #e0e0e0;
}
/* line 19, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick tbody > tr:nth-child(odd) > td {
background-color: #f9f9f9;
}
/* line 22, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick th[data-sorted="true"] {
-webkit-box-shadow: inset 1px 0 #bce8f1, inset -1px 0 #bce8f1;
-moz-box-shadow: inset 1px 0 #bce8f1, inset -1px 0 #bce8f1;
box-shadow: inset 1px 0 #bce8f1, inset -1px 0 #bce8f1;
color: #3a87ad;
background: #d9edf7;
border-bottom-color: #bce8f1;
}
/* line 28, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick th[data-sorted="true"]:first-child {
-webkit-box-shadow: inset -1px 0 #bce8f1;
-moz-box-shadow: inset -1px 0 #bce8f1;
box-shadow: inset -1px 0 #bce8f1;
}
/* line 31, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick th[data-sorted="true"]:last-child {
-webkit-box-shadow: inset 1px 0 #bce8f1;
-moz-box-shadow: inset 1px 0 #bce8f1;
box-shadow: inset 1px 0 #bce8f1;
}
/* line 34, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick th[data-sorted="true"][data-sorted-direction="descending"]:after {
border-top-color: #3a87ad;
}
/* line 37, ../sass/sortable-theme-slick.sass */
table[data-sortable].sortable-theme-slick th[data-sorted="true"][data-sorted-direction="ascending"]:after {
border-bottom-color: #3a87ad;
}

View file

@ -53,6 +53,27 @@ body {
height: 350px;
}
/* headers of sortable tables (static/js/sort_tables.js): the sorted one shows
its direction, up for ascending */
th[role=columnheader]:not([data-sort-method="none"]) {
cursor: pointer;
user-select: none;
}
th[aria-sort]:not([data-sort-method="none"])::after {
content: "";
display: inline-block;
margin-left: 0.4em;
vertical-align: middle;
border: 0.3em solid transparent;
border-top-color: currentColor;
}
th[aria-sort="ascending"]:not([data-sort-method="none"])::after {
border-top-color: transparent;
border-bottom-color: currentColor;
}
.host-collapsed tr[host] {
display: none;
}

6
static/js/sort_tables.js Normal file
View file

@ -0,0 +1,6 @@
// Every table marked data-sortable sorts by a click on, or Enter in, a column
// header (tablesort.min.js). Each table body sorts on its own; a row or
// header with data-sort-method="none" stays where it is.
document.querySelectorAll("table[data-sortable]").forEach(function (table) {
new Tablesort(table);
});

View file

@ -1,2 +0,0 @@
/*! sortable.js 0.8.0 */
(function(){var a,b,c,d,e,f,g;a="table[data-sortable]",d=/^-?[£$¤]?[\d,.]+%?$/,g=/^\s+|\s+$/g,c=["click"],f="ontouchstart"in document.documentElement,f&&c.push("touchstart"),b=function(a,b,c){return null!=a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)},e={init:function(b){var c,d,f,g,h;for(null==b&&(b={}),null==b.selector&&(b.selector=a),d=document.querySelectorAll(b.selector),h=[],f=0,g=d.length;g>f;f++)c=d[f],h.push(e.initTable(c));return h},initTable:function(a){var b,c,d,f,g,h;if(1===(null!=(h=a.tHead)?h.rows.length:void 0)&&"true"!==a.getAttribute("data-sortable-initialized")){for(a.setAttribute("data-sortable-initialized","true"),d=a.querySelectorAll("th"),b=f=0,g=d.length;g>f;b=++f)c=d[b],"false"!==c.getAttribute("data-sortable")&&e.setupClickableTH(a,c,b);return a}},setupClickableTH:function(a,d,f){var g,h,i,j,k,l;for(i=e.getColumnType(a,f),h=function(b){var c,g,h,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D;if(b.handled===!0)return!1;for(b.handled=!0,m="true"===this.getAttribute("data-sorted"),n=this.getAttribute("data-sorted-direction"),h=m?"ascending"===n?"descending":"ascending":i.defaultSortDirection,p=this.parentNode.querySelectorAll("th"),s=0,w=p.length;w>s;s++)d=p[s],d.setAttribute("data-sorted","false"),d.removeAttribute("data-sorted-direction");if(this.setAttribute("data-sorted","true"),this.setAttribute("data-sorted-direction",h),o=a.tBodies[0],l=[],m){for(D=o.rows,v=0,z=D.length;z>v;v++)g=D[v],l.push(g);for(l.reverse(),B=0,A=l.length;A>B;B++)k=l[B],o.appendChild(k)}else{for(r=null!=i.compare?i.compare:function(a,b){return b-a},c=function(a,b){return a[0]===b[0]?a[2]-b[2]:i.reverse?r(b[0],a[0]):r(a[0],b[0])},C=o.rows,j=t=0,x=C.length;x>t;j=++t)k=C[j],q=e.getNodeValue(k.cells[f]),null!=i.comparator&&(q=i.comparator(q)),l.push([q,k,j]);for(l.sort(c),u=0,y=l.length;y>u;u++)k=l[u],o.appendChild(k[1])}return"function"==typeof window.CustomEvent&&"function"==typeof a.dispatchEvent?a.dispatchEvent(new CustomEvent("Sortable.sorted",{bubbles:!0})):void 0},l=[],j=0,k=c.length;k>j;j++)g=c[j],l.push(b(d,g,h));return l},getColumnType:function(a,b){var c,d,f,g,h,i,j,k,l,m,n;if(d=null!=(l=a.querySelectorAll("th")[b])?l.getAttribute("data-sortable-type"):void 0,null!=d)return e.typesObject[d];for(m=a.tBodies[0].rows,h=0,j=m.length;j>h;h++)for(c=m[h],f=e.getNodeValue(c.cells[b]),n=e.types,i=0,k=n.length;k>i;i++)if(g=n[i],g.match(f))return g;return e.typesObject.alpha},getNodeValue:function(a){var b;return a?(b=a.getAttribute("data-value"),null!==b?b:"undefined"!=typeof a.innerText?a.innerText.replace(g,""):a.textContent.replace(g,"")):""},setupTypes:function(a){var b,c,d,f;for(e.types=a,e.typesObject={},f=[],c=0,d=a.length;d>c;c++)b=a[c],f.push(e.typesObject[b.name]=b);return f}},e.setupTypes([{name:"numeric",defaultSortDirection:"descending",match:function(a){return a.match(d)},comparator:function(a){return parseFloat(a.replace(/[^0-9.-]/g,""),10)||0}},{name:"date",defaultSortDirection:"ascending",reverse:!0,match:function(a){return!isNaN(Date.parse(a))},comparator:function(a){return Date.parse(a)||0}},{name:"alpha",defaultSortDirection:"ascending",match:function(){return!0},compare:function(a,b){return a.localeCompare(b)}}]),setTimeout(e.init,0),"function"==typeof define&&define.amd?define(function(){return e}):"undefined"!=typeof exports?module.exports=e:window.Sortable=e}).call(this);

6
static/js/tablesort.min.js vendored Normal file
View file

@ -0,0 +1,6 @@
/*!
* tablesort v5.7.1 (2026-04-15)
* http://tristen.ca/tablesort/demo/
* Copyright (c) 2026 ; Licensed MIT
*/
(()=>{function r(t,e){if(!(this instanceof r))return new r(t,e);if(!t||"TABLE"!==t.tagName)throw new Error("Element must be a table");this.init(t,e||{})}function v(t){var e;return window.CustomEvent&&"function"==typeof window.CustomEvent?e=new CustomEvent(t):(e=document.createEvent("CustomEvent")).initCustomEvent(t,!1,!1,void 0),e}function p(t,e){return e=e.sortAttribute||"data-sort",t.hasAttribute(e)?t.getAttribute(e):t.textContent||t.innerText||""}function A(t,e){return(t=t.trim().toLowerCase())===(e=e.trim().toLowerCase())?0:t<e?1:-1}function E(t,e){return[].slice.call(t).find(function(t){return t.getAttribute("data-sort-column-key")===e})}function x(n,o){return function(t,e){var r=n(t.td,e.td);return 0===r?o?e.index-t.index:t.index-e.index:r}}var y=[];r.extend=function(t,e,r){if("function"!=typeof e||"function"!=typeof r)throw new Error("Pattern and sort must be a function");y.push({name:t,pattern:e,sort:r})},r.prototype={init:function(t,e){var r,n,o,i=this;if(i.table=t,i.thead=!1,i.options=e,t.rows&&0<t.rows.length)if(t.tHead&&0<t.tHead.rows.length){for(a=0;a<t.tHead.rows.length;a++)if("thead"===t.tHead.rows[a].getAttribute("data-sort-method")){r=t.tHead.rows[a];break}r=r||t.tHead.rows[t.tHead.rows.length-1],i.thead=!0}else r=t.rows[0];if(r){for(var s=function(){i.current&&i.current!==this&&i.current.removeAttribute("aria-sort"),i.current=this,i.sortTable(this)},a=0;a<r.cells.length;a++)(o=r.cells[a]).setAttribute("role","columnheader"),"none"!==o.getAttribute("data-sort-method")&&(o.tabIndex=0,o.addEventListener("click",s,!1),o.addEventListener("keydown",function(t){"Enter"===t.key&&(t.preventDefault(),s.call(this))}),null!==o.getAttribute("data-sort-default"))&&(n=o);n&&(i.current=n,i.sortTable(n))}},sortTable:function(t,e){var r=this,n=t.getAttribute("data-sort-column-key"),o=t.cellIndex,i=A,s="",a=[],d=r.thead?0:1,u=t.getAttribute("data-sort-method"),l=t.hasAttribute("data-sort-reverse"),c=t.getAttribute("aria-sort");if(r.table.dispatchEvent(v("beforeSort")),e||(c="ascending"===c||"descending"!==c&&!!r.options.descending!=l?"descending":"ascending",t.setAttribute("aria-sort",c)),!(r.table.rows.length<2)){if(!u){for(;a.length<3&&d<r.table.tBodies[0].rows.length;)0<(s=(s=(h=n?E(r.table.tBodies[0].rows[d].cells,n):r.table.tBodies[0].rows[d].cells[o])?p(h,r.options):"").trim()).length&&a.push(s),d++;if(!a)return}for(d=0;d<y.length;d++)if(s=y[d],u){if(s.name===u){i=s.sort;break}}else if(a.every(s.pattern)){i=s.sort;break}for(r.col=o,d=0;d<r.table.tBodies.length;d++){var f,h,b=[],w={},g=0,m=0;if(!(r.table.tBodies[d].rows.length<2)){for(f=0;f<r.table.tBodies[d].rows.length;f++)"none"===(s=r.table.tBodies[d].rows[f]).getAttribute("data-sort-method")?w[g]=s:(h=n?E(s.cells,n):s.cells[r.col],b.push({tr:s,td:h?p(h,r.options):"",index:g})),g++;for("descending"===c?b.sort(x(i,!0)):(b.sort(x(i,!1)),b.reverse()),f=0;f<g;f++)w[f]?(s=w[f],m++):s=b[f-m].tr,r.table.tBodies[d].appendChild(s)}}r.table.dispatchEvent(v("afterSort"))}},refresh:function(){void 0!==this.current&&this.sortTable(this.current,!0)}},"undefined"!=typeof module&&module.exports?module.exports=r:window.Tablesort=r})();

6
static/js/tablesort.number.min.js vendored Normal file
View file

@ -0,0 +1,6 @@
/*!
* tablesort v5.7.1 (2026-04-15)
* http://tristen.ca/tablesort/demo/
* Copyright (c) 2026 ; Licensed MIT
*/
(()=>{function a(t){return t.replace(/[^\-?0-9.]/g,"")}Tablesort.extend("number",function(t){return t.match(/^[-+]?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/)||t.match(/^[-+]?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/)||t.match(/^[-+]?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/)},function(t,e){return t=a(t),e=a(e),e=e,t=t,e=parseFloat(e),t=parseFloat(t),(e=isNaN(e)?0:e)-(t=isNaN(t)?0:t)})})();

View file

@ -4,9 +4,6 @@
{% load static %}
{% load bootstrap_icons %}
{% block title %}{{ compute }} - {% trans "Storage" %}: {{ pool }}{% endblock %}
{% block style %}
<link rel="stylesheet" href="{% static "css/sortable-theme-bootstrap.css" %}"/>
{% endblock %}
{% block page_heading %}{{ compute }} - {% trans "Storage" %}: {{ pool }}{% endblock page_heading %}
@ -95,15 +92,15 @@
</div>
{% if volumes %}
<div class="table-responsive">
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
<table class="table table-hover" data-sortable>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" data-sort-method="number">#</th>
<th scope="col">{% trans "Name" %}</th>
<th scope="col">{% trans "Allocated" %}</th>
<th scope="col">{% trans "Size" %}</th>
<th scope="col" data-sort-method="number">{% trans "Allocated" %}</th>
<th scope="col" data-sort-method="number">{% trans "Size" %}</th>
<th scope="col">{% trans "Format" %}</th>
<th scope="colgroup" data-sortable="false" colspan="2">{% trans "Action" %}</th>
<th scope="colgroup" data-sort-method="none" colspan="2">{% trans "Action" %}</th>
</tr>
</thead>
<tbody class="searchable">
@ -111,8 +108,8 @@
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ volume.name|truncatechars:55 }}</td>
<td>{{ volume.allocation|filesizeformat }}</td>
<td>{{ volume.size|filesizeformat }}</td>
<td data-sort="{{ volume.allocation }}">{{ volume.allocation|filesizeformat }}</td>
<td data-sort="{{ volume.size }}">{{ volume.size|filesizeformat }}</td>
<td>{{ volume.type }}</td>
<td>
<!-- Modal Clone -->
@ -201,7 +198,6 @@
{% block script %}
<script src="{% static 'js/poll.js' %}"></script>
{% job_panel_script job_resource %}
<script src="{% static 'js/sortable.min.js' %}"></script>
<script src="{% static 'js/filter-table.js' %}"></script>
<script>
$('.format-convert').hide();

View file

@ -66,6 +66,10 @@
<script src="{% static 'js/bootstrap.bundle.min.js' %}"></script>
<!-- JavaScript Cookie -->
<script src="{% static 'js/js.cookie.js' %}"></script>
<!-- Table sorting -->
<script src="{% static 'js/tablesort.min.js' %}"></script>
<script src="{% static 'js/tablesort.number.min.js' %}"></script>
<script src="{% static 'js/sort_tables.js' %}"></script>
<script>
// After a refused submission, start at the error. The places an error
// can be, most specific first: a refused field, its form's summary, an

View file

@ -4,9 +4,6 @@
{% load bootstrap_icons %}
{% block title %}{% trans "Secrets" %} - {{ compute.name }}{% endblock %}
{% block style %}
<link rel="stylesheet" href="{% static 'css/sortable-theme-bootstrap.css' %}" />
{% endblock %}
{% block page_heading %}{{ compute }} - {% trans "Secrets" %}{% endblock page_heading %}
@ -55,13 +52,13 @@
{% else %}
<div class="col-lg-12">
<div class="table-responsive">
<table class="table table-hover sortable-theme-bootstrap" data-sortable>
<table class="table table-hover" data-sortable>
<thead>
<tr class="active">
<th scope="col">{% trans "UUID" %}</th>
<th scope="col">{% trans "Usage Type" %}</th>
<th scope="col">{% trans "Data" %}</th>
<th scope="col" data-sortable="false" style="width:90px;">{% trans "Action" %}</th>
<th scope="col" data-sort-method="none" style="width:90px;">{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
@ -118,6 +115,3 @@
{% endif %}
</div>
{% endblock %}
{% block script %}
<script src="{% static "js/sortable.min.js" %}"></script>
{% endblock %}