mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-11 12:46:49 +00:00
fix(templates): the list pages search their rows as text, with one script
The list pages' search (filter-table.js) read what was typed as a
regular expression: a "(" or "[" stopped it with an error. It also
needed jQuery, while the instance list had its own script for the same
search. Both are now static/js/table_filter.js: plain text in any case,
the instance list's state list and host groups where a page has them,
remembered for a day as before. A group's header row is marked as one
(data-group-header) instead of being any row without a state.
This commit is contained in:
parent
b4c999a22f
commit
e5c7dc338d
14 changed files with 58 additions and 56 deletions
|
|
@ -57,5 +57,5 @@
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
|
|
@ -61,5 +61,5 @@
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
|
|
@ -85,5 +85,5 @@
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static "js/filter-table.js" %}"></script>
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
|
|
@ -113,5 +113,5 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'js/poll.js' %}"></script>
|
<script src="{% static 'js/poll.js' %}"></script>
|
||||||
{% job_panel_script job_resource %}
|
{% job_panel_script job_resource %}
|
||||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
@ -106,5 +106,5 @@
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'js/instance_filter.js' %}"></script>
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||||
<script src="{% static 'js/find_address.js' %}"></script>
|
<script src="{% static 'js/find_address.js' %}"></script>
|
||||||
{% if request.user.is_superuser %}
|
{% if request.user.is_superuser %}
|
||||||
<script>
|
<script>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
{% for compute in computes %}
|
{% for compute in computes %}
|
||||||
<tbody class="searchable" data-host="{{ compute.name }}">
|
<tbody class="searchable" data-host="{{ compute.name }}">
|
||||||
{% if compute.instance_set.count > 0 %}
|
{% if compute.instance_set.count > 0 %}
|
||||||
<tr class="table-secondary fw-bold border-bottom border-dark" data-sort-method="none">
|
<tr class="table-secondary fw-bold border-bottom border-dark" data-sort-method="none" data-group-header>
|
||||||
<td>
|
<td>
|
||||||
<span id="collapse_host_instances_{{ compute.id }}" onclick="hide_host_instances('{{ compute.id }}');" class="bi bi-chevron-up" ></span>
|
<span id="collapse_host_instances_{{ compute.id }}" onclick="hide_host_instances('{{ compute.id }}');" class="bi bi-chevron-up" ></span>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
// a collapsed host's VM rows are hidden by its table body's class, so the
|
// a collapsed host's VM rows are hidden by its table body's class, so the
|
||||||
// search (instance_filter.js, the hidden attribute) can still hide them too
|
// search (table_filter.js, the hidden attribute) can still hide them too
|
||||||
function hide_all_host_instances() {
|
function hide_all_host_instances() {
|
||||||
$('tbody.searchable').toggleClass('host-collapsed');
|
$('tbody.searchable').toggleClass('host-collapsed');
|
||||||
$('span[id^=collapse_host_instances_]').toggleClass("bi bi-chevron-down ").toggleClass("bi bi-chevron-up");
|
$('span[id^=collapse_host_instances_]').toggleClass("bi bi-chevron-down ").toggleClass("bi bi-chevron-up");
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,26 @@
|
||||||
// node instance_filter.test.js path/to/static/js/instance_filter.js
|
// node table_filter.test.js path/to/static/js/table_filter.js
|
||||||
const assert = require("assert");
|
const assert = require("assert");
|
||||||
const {filterInstances} = require(process.argv[2]);
|
const {filterTable: filterInstances} = require(process.argv[2]);
|
||||||
|
|
||||||
// A minimal DOM: table bodies of rows; a VM row holds its state mark.
|
// A minimal DOM: table bodies of rows; a VM row holds its state mark, a
|
||||||
|
// group's header row is marked as one.
|
||||||
function vm(text, state) {
|
function vm(text, state) {
|
||||||
const mark = {dataset: {state}};
|
const mark = {dataset: {state}};
|
||||||
return {textContent: text, hidden: false, querySelector: (s) => (s === "[data-state]" ? mark : null)};
|
return {textContent: text, hidden: false, hasAttribute: () => false,
|
||||||
|
querySelector: (s) => (s === "[data-state]" ? mark : null)};
|
||||||
}
|
}
|
||||||
function hostRow(text) {
|
function hostRow(text) {
|
||||||
return {textContent: text, hidden: false, querySelector: () => null};
|
return {textContent: text, hidden: false, hasAttribute: (a) => a === "data-group-header", querySelector: () => null};
|
||||||
|
}
|
||||||
|
function plainRow(text) {
|
||||||
|
return {textContent: text, hidden: false, hasAttribute: () => false, querySelector: () => null};
|
||||||
}
|
}
|
||||||
function group(rows, host) {
|
function group(rows, host) {
|
||||||
return {dataset: host ? {host} : {}, querySelectorAll: (s) => (s === "tr" ? rows : [])};
|
return {dataset: host ? {host} : {}, querySelectorAll: (s) => (s === "tr" ? rows : [])};
|
||||||
}
|
}
|
||||||
function page(groups, text, state) {
|
function page(groups, text, state) {
|
||||||
const inputs = {filter: {value: text}, "state-filter": {value: state}};
|
// a list without a state list (state undefined) has no such element
|
||||||
|
const inputs = {filter: {value: text}, "state-filter": state === undefined ? undefined : {value: state}};
|
||||||
return {
|
return {
|
||||||
getElementById: (id) => inputs[id],
|
getElementById: (id) => inputs[id],
|
||||||
querySelectorAll: (s) => (s === ".searchable" ? groups : []),
|
querySelectorAll: (s) => (s === ".searchable" ? groups : []),
|
||||||
|
|
@ -49,3 +55,12 @@ filterInstances(page(groups, "host-a", "Off"));
|
||||||
assert.deepStrictEqual(visible(a), ["host-a Connected", "db-1 Off"], "the state applies to a host's VMs too");
|
assert.deepStrictEqual(visible(a), ["host-a Connected", "db-1 Off"], "the state applies to a host's VMs too");
|
||||||
filterInstances(page(groups, "Connected", ""));
|
filterInstances(page(groups, "Connected", ""));
|
||||||
assert.deepStrictEqual(visible(a), [], "a host row's other text is not its name");
|
assert.deepStrictEqual(visible(a), [], "a host row's other text is not its name");
|
||||||
|
|
||||||
|
// any other list: no state list, no groups; its rows are filtered by their text
|
||||||
|
const plain = [plainRow("alice Active"), plainRow("bob (vda) Blocked"), plainRow("carol Active")];
|
||||||
|
filterInstances(page([group(plain)], "ACTIVE"));
|
||||||
|
assert.deepStrictEqual(visible(plain), ["alice Active", "carol Active"], "a plain list is searched");
|
||||||
|
filterInstances(page([group(plain)], "(vda"));
|
||||||
|
assert.deepStrictEqual(visible(plain), ["bob (vda) Blocked"], "a bracket is text there too");
|
||||||
|
filterInstances(page([group(plain)], ""));
|
||||||
|
assert.deepStrictEqual(visible(plain), ["alice Active", "bob (vda) Blocked", "carol Active"]);
|
||||||
|
|
@ -120,7 +120,7 @@ class StateTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
def test_the_list_offers_each_state_and_marks_each_row_and_host(self):
|
def test_the_list_offers_each_state_and_marks_each_row_and_host(self):
|
||||||
# static/js/instance_filter.js filters by these (test_instance_filter_js)
|
# static/js/table_filter.js filters by these (test_table_filter_js)
|
||||||
AppSettings.objects.filter(key="VIEW_INSTANCES_LIST_STYLE").update(value="grouped")
|
AppSettings.objects.filter(key="VIEW_INSTANCES_LIST_STYLE").update(value="grouped")
|
||||||
page = html.fromstring(self.get(self.admin, reverse("instances:index")))
|
page = html.fromstring(self.get(self.admin, reverse("instances:index")))
|
||||||
options = page.xpath("//select[@id='state-filter']/option/text()")
|
options = page.xpath("//select[@id='state-filter']/option/text()")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""The instance list's search box and state list (static/js/instance_filter.js).
|
"""The search box of the list pages, with the instance list's state list
|
||||||
Its behaviour is checked with Node."""
|
(static/js/table_filter.js). Its behaviour is checked with Node."""
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
@ -11,11 +11,11 @@ from django.conf import settings
|
||||||
BASE = Path(settings.BASE_DIR)
|
BASE = Path(settings.BASE_DIR)
|
||||||
|
|
||||||
|
|
||||||
class InstanceFilterTestCase(unittest.TestCase):
|
class TableFilterTestCase(unittest.TestCase):
|
||||||
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
@unittest.skipUnless(shutil.which("node"), "node is not installed")
|
||||||
def test_instance_filter_js(self):
|
def test_table_filter_js(self):
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["node", str(BASE / "instances/tests/js/instance_filter.test.js"), str(BASE / "static/js/instance_filter.js")],
|
["node", str(BASE / "instances/tests/js/table_filter.test.js"), str(BASE / "static/js/table_filter.js")],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=60,
|
timeout=60,
|
||||||
|
|
@ -140,7 +140,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||||
<script src="{% static 'js/ace/ace.js' %}"></script>
|
<script src="{% static 'js/ace/ace.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
var editor = ace.edit("editor");
|
var editor = ace.edit("editor");
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
function filter_table() {
|
|
||||||
var rex = new RegExp($(this).val(), 'i');
|
|
||||||
$('.searchable tr').hide();
|
|
||||||
$('.searchable tr').filter(function () {
|
|
||||||
return rex.test($(this).text());
|
|
||||||
}).show();
|
|
||||||
Cookies.set(document.title + "_filter", $(this).val(), { expires: 1 });
|
|
||||||
}
|
|
||||||
$(document).ready(function () {
|
|
||||||
filter_cookie = Cookies.get(document.title + "_filter");
|
|
||||||
if (filter_cookie) {
|
|
||||||
$('#filter').val(filter_cookie);
|
|
||||||
$('#filter').each(filter_table);
|
|
||||||
}
|
|
||||||
(function ($) {
|
|
||||||
$('#filter').keyup(filter_table)
|
|
||||||
}(jQuery));
|
|
||||||
});
|
|
||||||
|
|
@ -1,22 +1,25 @@
|
||||||
// The instance list's search box and state list.
|
// The search box of a list page, and the instance list's state list.
|
||||||
//
|
//
|
||||||
// A VM row shows when its text contains the search, in any case, and the VM is
|
// A row shows when its text contains the search, in any case: plain text,
|
||||||
// in the chosen state. In the grouped list a search for a host's name shows
|
// not a pattern. With a state list, the row's [data-state] must be the
|
||||||
// all of its VMs, and the host's row shows while any of its VMs does.
|
// chosen state too. In a grouped list a search for a group's host shows all
|
||||||
function filterInstances(doc) {
|
// of its rows, and its header row (data-group-header) shows while any of its
|
||||||
|
// rows does. The search is remembered for a day, per page.
|
||||||
|
function filterTable(doc) {
|
||||||
var text = doc.getElementById("filter").value.toLowerCase();
|
var text = doc.getElementById("filter").value.toLowerCase();
|
||||||
var state = doc.getElementById("state-filter").value;
|
var states = doc.getElementById("state-filter");
|
||||||
|
var state = states ? states.value : "";
|
||||||
doc.querySelectorAll(".searchable").forEach(function (group) {
|
doc.querySelectorAll(".searchable").forEach(function (group) {
|
||||||
var host = group.dataset.host;
|
var host = group.dataset.host;
|
||||||
var hostMatches = host !== undefined && host.toLowerCase().includes(text);
|
var hostMatches = host !== undefined && host.toLowerCase().includes(text);
|
||||||
var header = null, shown = false;
|
var header = null, shown = false;
|
||||||
group.querySelectorAll("tr").forEach(function (row) {
|
group.querySelectorAll("tr").forEach(function (row) {
|
||||||
var mark = row.querySelector("[data-state]");
|
if (row.hasAttribute("data-group-header")) {
|
||||||
if (!mark) {
|
|
||||||
header = row;
|
header = row;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
row.hidden = !((!state || mark.dataset.state === state) &&
|
var mark = row.querySelector("[data-state]");
|
||||||
|
row.hidden = !((!state || (mark && mark.dataset.state === state)) &&
|
||||||
(hostMatches || row.textContent.toLowerCase().includes(text)));
|
(hostMatches || row.textContent.toLowerCase().includes(text)));
|
||||||
shown = shown || !row.hidden;
|
shown = shown || !row.hidden;
|
||||||
});
|
});
|
||||||
|
|
@ -27,17 +30,19 @@ function filterInstances(doc) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof module !== "undefined") {
|
if (typeof module !== "undefined") {
|
||||||
module.exports = {filterInstances: filterInstances};
|
module.exports = {filterTable: filterTable};
|
||||||
} else {
|
} else {
|
||||||
// the search is remembered for a day, as on the other lists
|
|
||||||
var search = document.getElementById("filter");
|
var search = document.getElementById("filter");
|
||||||
|
var states = document.getElementById("state-filter");
|
||||||
search.value = Cookies.get(document.title + "_filter") || "";
|
search.value = Cookies.get(document.title + "_filter") || "";
|
||||||
search.addEventListener("input", function () {
|
search.addEventListener("input", function () {
|
||||||
Cookies.set(document.title + "_filter", search.value, {expires: 1});
|
Cookies.set(document.title + "_filter", search.value, {expires: 1});
|
||||||
filterInstances(document);
|
filterTable(document);
|
||||||
});
|
});
|
||||||
document.getElementById("state-filter").addEventListener("change", function () {
|
if (states) {
|
||||||
filterInstances(document);
|
states.addEventListener("change", function () {
|
||||||
});
|
filterTable(document);
|
||||||
filterInstances(document);
|
});
|
||||||
|
}
|
||||||
|
filterTable(document);
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +198,7 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script src="{% static 'js/poll.js' %}"></script>
|
<script src="{% static 'js/poll.js' %}"></script>
|
||||||
{% job_panel_script job_resource %}
|
{% job_panel_script job_resource %}
|
||||||
<script src="{% static 'js/filter-table.js' %}"></script>
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
$('.format-convert').hide();
|
$('.format-convert').hide();
|
||||||
$(document).on('change', '.volume-convert', function () {
|
$(document).on('change', '.volume-convert', function () {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue