mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 20:26:48 +00:00
- The search read a row's whole text, so a word on a button or in a state label matched every row. VM rows now carry data-search (the name, title and owners, and the host in the flat list), and table_filter.js searches that text when a row has it; the state has its own filter. - Nothing matching left an empty table. table_filter.js shows the page's #no-match while no row does; the VM list's says so and offers "Clear the filters". A user with no VMs at all is told so instead of an empty table. - The grouped list's "#" toggled each host on its own: a host collapsed by hand opened while the others closed. It now collapses every host, or opens them all once all are collapsed. - One alert names every host the panel cannot reach, with why, in place of one dismissible alert per host; users who see no hosts keep their own message. - A host's VM list showed a VM's maximum memory, the Instances page its current memory: both show the current memory now. - The owners' users are prefetched with the VMs; the flat list's Bootstrap 4 font-italic is fst-italic.
164 lines
9 KiB
HTML
164 lines
9 KiB
HTML
{% extends "base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
{% block title %}{% trans "Instances" %}{% endblock %}
|
|
{% block page_heading %}{% trans "Instances" %}{% endblock page_heading %}
|
|
|
|
{% block page_heading_extra %}
|
|
{% if request.user.is_superuser %}
|
|
{% include 'create_inst_block.html' %}
|
|
{% endif %}
|
|
{# the list's filters (state, text) and the address search, as one control #}
|
|
<div class="float-end m-1">
|
|
<div class="input-group">
|
|
<select id="state-filter" class="form-select flex-grow-0 w-auto" aria-label="{% trans 'State' %}">
|
|
<option value="">{% trans 'All states' %}</option>
|
|
{% for state in states %}
|
|
<option>{{ state }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<input id="filter" class="form-control" type="text" placeholder="{% trans 'Search' %}" aria-label="{% trans 'Search' %}">
|
|
<button type="button" class="btn btn-outline-secondary" id="find-address-open"
|
|
data-bs-toggle="modal" data-bs-target="#find-address">{% trans "Find IP" %}</button>
|
|
</div>
|
|
</div>
|
|
{% endblock page_heading_extra %}
|
|
|
|
{% block content %}
|
|
{% if show_hosts %}
|
|
{% if unreachable_computes %}
|
|
{# one alert for every host the panel cannot reach, each with why #}
|
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
|
<div class="fw-semibold">{% blocktrans count counter=unreachable_computes|length %}The panel cannot reach {{ counter }} host:{% plural %}The panel cannot reach {{ counter }} hosts:{% endblocktrans %}</div>
|
|
<ul class="mb-0">
|
|
{% for compute in unreachable_computes %}
|
|
<li><span class="fw-semibold">{{ compute.name }}</span>: {{ compute.connection_error }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans "Close" %}"></button>
|
|
</div>
|
|
{% endif %}
|
|
{% elif own_hosts_down %}
|
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
|
{% trans 'A host of some of your virtual machines cannot be reached right now.' %}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
{% endif %}
|
|
<div class="col-lg-12">
|
|
{% if not instances.exists %}
|
|
<p class="text-center text-body-secondary py-4">{% trans "There are no VMs here yet." %}</p>
|
|
{% elif app_settings.VIEW_INSTANCES_LIST_STYLE == 'grouped' and request.user.is_superuser %}
|
|
{% include 'allinstances_index_grouped.html' %}
|
|
{% else %}
|
|
{% include 'allinstances_index_nongrouped.html' %}
|
|
{% endif %}
|
|
{# table_filter.js shows it while no row matches #}
|
|
<div id="no-match" class="text-center text-body-secondary py-4" hidden>
|
|
{% trans "No VM matches the search and the state." %}
|
|
<button type="button" class="btn btn-link" id="clear-filters">{% trans "Clear the filters" %}</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal fade" id="find-address" tabindex="-1" aria-labelledby="find-address-title" aria-hidden="true"
|
|
data-url="{% url 'instances:find_address' %}" data-failed="{% trans "The search did not go through. Try again." %}"
|
|
data-none="{% trans "No running VM reports this address." %}"
|
|
data-none-mac="{% trans "No VM has a network card with this MAC address." %}"
|
|
data-unregistered="{% trans "(not registered in the panel)" %}" data-conflict="{% trans "possible conflict" %}"
|
|
data-checked="{% trans "Running VMs checked: %(n)s." %}"
|
|
data-no-agent="{% trans "%(n)s had no connected guest agent; a static address on them cannot be seen." %}"
|
|
data-unread="{% trans "Guest agents not read (a paused VM, no answer in time, or an error): %(n)s." %}"
|
|
data-hosts-down="{% trans "Not reached: %(hosts)s." %}"
|
|
data-hosts-unfinished="{% trans "Not finished in time: %(hosts)s." %}"
|
|
data-read-at="{% trans "Read at %(time)s." %}"
|
|
data-some-unchecked="{% trans "Some of your VMs could not be checked." %}">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title h5" id="find-address-title">{% trans "Find the VMs using an IP or MAC address" %}</h2>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="find-address-form" class="d-flex gap-2 mb-3">
|
|
<label class="visually-hidden" for="find-address-q">{% trans "IP address, network or MAC address" %}</label>
|
|
<input type="text" class="form-control" name="q" id="find-address-q" required
|
|
placeholder="192.0.2.10, 192.0.2.0/24 {% trans "or" %} 52:54:00:12:34:56">
|
|
<button type="submit" class="btn btn-primary">{% trans "Find" %}</button>
|
|
</form>
|
|
<div class="alert alert-danger d-none" role="alert" id="find-address-error"></div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm d-none align-middle" id="find-address-results">
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">{% trans "Address" %}</th>
|
|
<th scope="col">{% trans "VM" %}</th>
|
|
<th scope="col">{% trans "MAC" %}</th>
|
|
<th scope="col">{% trans "Source" %}</th>
|
|
{% if show_hosts %}<th scope="col">{% trans "Host" %}</th>{% endif %}
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
<p class="small text-body-secondary mb-0" id="find-address-coverage"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock content %}
|
|
{% block script %}
|
|
<script src="{% static 'js/table_filter.js' %}"></script>
|
|
<script src="{% static 'js/find_address.js' %}"></script>
|
|
{% if request.user.is_superuser %}
|
|
<script>
|
|
// The new-VM dialog's CPU bars and capacity lines, read when the
|
|
// dialog opens, two reads at a time: a CPU read holds a worker for
|
|
// a second, a capacity read while it reads the host's VMs. Answers
|
|
// of an earlier opening are ignored.
|
|
const addInstance = document.getElementById("AddInstance");
|
|
let opening = 0, queue = [], reading = 0;
|
|
|
|
function setCpuUsage(cell, percent, text) {
|
|
const meter = cell.querySelector(".progress");
|
|
cell.querySelector(".progress-bar").style.width = (percent || 0) + "%";
|
|
if (percent === null) {
|
|
meter.removeAttribute("aria-valuenow");
|
|
} else {
|
|
meter.setAttribute("aria-valuenow", percent);
|
|
}
|
|
cell.querySelector(".cpu-usage-value").textContent = text;
|
|
}
|
|
|
|
// data: the answer, or null when the read failed
|
|
function show(element, data) {
|
|
if (element.classList.contains("compute-capacity")) {
|
|
element.textContent = data === null ? "" : data.trim();
|
|
return;
|
|
}
|
|
const usage = data !== null && typeof data.cpudata === "number" ? Math.round(data.cpudata) : null;
|
|
setCpuUsage(element, usage, usage === null ? "—" : usage + "%");
|
|
}
|
|
|
|
function readNext() {
|
|
while (reading < 2 && queue.length) {
|
|
const element = queue.shift(), current = opening;
|
|
reading++;
|
|
$.get(element.dataset.url)
|
|
.done((data) => { if (current === opening) show(element, data); })
|
|
.fail(() => { if (current === opening) show(element, null); })
|
|
.always(() => { reading--; readNext(); });
|
|
}
|
|
}
|
|
|
|
addInstance.addEventListener("show.bs.modal", function () {
|
|
opening++;
|
|
addInstance.querySelectorAll(".cpu-usage").forEach((cell) => setCpuUsage(cell, null, "…"));
|
|
// host by host, as the list shows them
|
|
queue = Array.from(addInstance.querySelectorAll(".cpu-usage, .compute-capacity"));
|
|
readNext();
|
|
});
|
|
addInstance.addEventListener("hide.bs.modal", function () {
|
|
opening++;
|
|
queue = [];
|
|
});
|
|
</script>
|
|
{% endif %}
|
|
{% endblock script %}
|