mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +00:00
refactor(instances): the VM page's scripts load from static files
The VM page's thirteen inline script blocks and its inline handlers move to static files: instance_page.js (tabs, OS details, clone names and MACs, Add disk dialog, boot order, host devices, migrate plan, console window, switches that send their form), instance_stats.js (charts and the Logs view) and instance_status.js (the reload when the VM's state changes). Their values and messages come as data attributes of the elements they belong to, page-wide ones on #vm-summary; a form marked data-please-wait shows the please-wait dialog. Three blocks that did nothing are dropped: popovers (the page has none), the video model preselection (the server selects it) and the delete form's poll stop (that form is on the destroy page). The HTML sink inventory now also reads the app's own scripts.
This commit is contained in:
parent
393aecb8f1
commit
561937b31a
18 changed files with 507 additions and 460 deletions
280
static/js/instance_page.js
Normal file
280
static/js/instance_page.js
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
// The VM page (instance.html and its tabs). Page-wide values are data
|
||||
// attributes of #vm-summary; the others are on the element they belong to.
|
||||
(function () {
|
||||
var summary = document.getElementById("vm-summary");
|
||||
|
||||
// Open the tab whose control targets "#<pane id>"
|
||||
function show_tab(target) {
|
||||
var controls = document.querySelectorAll('[data-bs-target], a[data-bs-toggle][href]');
|
||||
for (var i = 0; i < controls.length; i++) {
|
||||
var c = controls[i];
|
||||
if ((c.getAttribute('data-bs-target') || c.getAttribute('href')) === target) {
|
||||
bootstrap.Tab.getOrCreateInstance(c).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function on(id, event, handler) {
|
||||
var element = document.getElementById(id);
|
||||
if (element) element.addEventListener(event, handler);
|
||||
}
|
||||
|
||||
// the guest OS details, read through the guest agent
|
||||
function load_osinfo() {
|
||||
var error = $('#osinfo-error');
|
||||
var silent = error[0].dataset.silent;
|
||||
$.getJSON(summary.dataset.osinfoUrl, function (data) {
|
||||
error.addClass('d-none');
|
||||
$('#oshostname').text(data['host-name'] || '');
|
||||
$('#osname').text(data.id || '');
|
||||
$('#osprettyname').text(data['pretty-name'] || '');
|
||||
$('#oskernelrelease').text(data['kernel-release'] || '');
|
||||
$('#oskernelversion').text(data['kernel-version'] || '');
|
||||
$('#osversion').text(data.version || '');
|
||||
$('#ostimezone').text(data.zone ? data.zone + " / " + data.offset : '');
|
||||
}).fail(function (xhr) {
|
||||
var message = xhr.responseJSON && xhr.responseJSON.error;
|
||||
error.text(message || silent).removeClass('d-none');
|
||||
});
|
||||
}
|
||||
function get_osinfo() {
|
||||
show_tab('#settings');
|
||||
show_tab('#osinfo');
|
||||
load_osinfo();
|
||||
}
|
||||
on("os-details-button", "click", get_osinfo);
|
||||
on("osinfo-tab", "click", get_osinfo);
|
||||
on("clone-button", "click", function () {
|
||||
show_tab('#settings');
|
||||
show_tab('#clone');
|
||||
});
|
||||
|
||||
// one window per VM: opening it again reloads it
|
||||
on("console_button", "click", function () {
|
||||
window.open(this.dataset.url, this.dataset.window, 'width=850,height=650');
|
||||
});
|
||||
if (summary.dataset.vdiUrl) {
|
||||
$.get(summary.dataset.vdiUrl, function (data) {
|
||||
$("#vdi_url_input").attr("value", data);
|
||||
$("#vdi_url").attr("href", data);
|
||||
});
|
||||
}
|
||||
|
||||
// a switch whose form is sent as soon as it is flipped
|
||||
document.querySelectorAll("[data-submit-on-change]").forEach(function (input) {
|
||||
input.addEventListener("change", function () { input.form.submit(); });
|
||||
});
|
||||
|
||||
// a memory snapshot freezes the guest by itself: the freeze option waits
|
||||
// until memory is unchecked, then is what it was
|
||||
on("snapshot-memory", "change", function () {
|
||||
var quiesce = document.getElementById('snapshot-quiesce');
|
||||
if (!quiesce.dataset.offered) return;
|
||||
if (this.checked) quiesce.dataset.was = quiesce.checked;
|
||||
quiesce.disabled = this.checked;
|
||||
quiesce.checked = !this.checked && quiesce.dataset.was !== 'false';
|
||||
});
|
||||
|
||||
// the Add disk dialog: the form goes where the chosen source is made,
|
||||
// and an existing volume is chosen from the volumes of its pool
|
||||
var addvol = document.getElementById('addvol');
|
||||
if (addvol) {
|
||||
var addvolForm = document.getElementById('addvol-form');
|
||||
choiceDialog(addvol, 'disk_source', function (source) {
|
||||
addvolForm.action = addvolForm.dataset[source];
|
||||
});
|
||||
}
|
||||
var pool = document.getElementById("selected_storage");
|
||||
if (pool) {
|
||||
pool.addEventListener("change", function () {
|
||||
$.getJSON(pool.dataset.volumesUrl.replace("POOL", encodeURIComponent(pool.value)), function (data) {
|
||||
var vols = $("#vols").empty();
|
||||
$.each(data['vols'], function (i, item) {
|
||||
vols.append($("<option>").val(item).text(item));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// MAC addresses and names of a clone or a new NIC
|
||||
function random_mac(name) {
|
||||
$.getJSON(summary.dataset.randomMacUrl, function (data) {
|
||||
$('input[name="' + name + '"]').val(data['mac']);
|
||||
});
|
||||
}
|
||||
document.querySelectorAll("[data-random-mac]").forEach(function (button) {
|
||||
button.addEventListener("click", function () { random_mac(button.dataset.randomMac); });
|
||||
});
|
||||
var cloneName = document.getElementById("clone_name");
|
||||
// the DHCP host entry of the clone's name holds one address: the first NIC's
|
||||
function guess_mac_address() {
|
||||
$.getJSON(cloneName.dataset.guessMacUrl.replace("NAME", encodeURIComponent(cloneName.value)), function (data) {
|
||||
$('input[name="clone-net-mac-0"]').val(data['mac']);
|
||||
});
|
||||
}
|
||||
// each disk of the clone is named after the clone
|
||||
function update_clone_disk_name(new_vname) {
|
||||
var vname = cloneName.dataset.source;
|
||||
document.querySelectorAll("input[data-image]").forEach(function (input) {
|
||||
var disk_name = input.dataset.image;
|
||||
var disk_dot = disk_name.split('.');
|
||||
var disk_dot_suffix = disk_dot[disk_dot.length - 1];
|
||||
var image;
|
||||
if (disk_name.startsWith(vname)) {
|
||||
image = disk_name.replace(vname, new_vname);
|
||||
} else if (disk_name.lastIndexOf('.') > -1 && disk_dot_suffix.length <= 7) {
|
||||
disk_dot.pop();
|
||||
image = new_vname + "-" + disk_dot.join('-') + "." + disk_dot_suffix;
|
||||
} else if (new_vname != disk_name) {
|
||||
image = new_vname;
|
||||
} else {
|
||||
image = new_vname + '-clone';
|
||||
}
|
||||
input.value = image;
|
||||
});
|
||||
}
|
||||
function guess_clone_name() {
|
||||
var help = $('#clone-name-help');
|
||||
var noName = help[0].dataset.noName;
|
||||
$.getJSON(cloneName.dataset.guessUrl, function (data) {
|
||||
// a free name of the DHCP file, if there is one
|
||||
if (!data.name) {
|
||||
help.text(noName);
|
||||
return;
|
||||
}
|
||||
help.text('');
|
||||
cloneName.value = data['name'].split(".")[0];
|
||||
update_clone_disk_name(cloneName.value);
|
||||
guess_mac_address();
|
||||
});
|
||||
}
|
||||
if (cloneName) {
|
||||
cloneName.addEventListener("input", function () { update_clone_disk_name(cloneName.value); });
|
||||
on("guess-clone-name", "click", guess_clone_name);
|
||||
on("guess-clone-mac", "click", guess_mac_address);
|
||||
update_clone_disk_name(cloneName.value);
|
||||
}
|
||||
// every NIC of the clone a new address: one the VM has would clash with it
|
||||
document.querySelectorAll("[data-new-mac]").forEach(function (input) { random_mac(input.name); });
|
||||
|
||||
// the boot order: the checked devices, top to bottom, as "disk:vda,cdrom:sda"
|
||||
function set_orderlist() {
|
||||
var order = $('#b_order input:checked').map(function () { return this.value; }).get();
|
||||
$('#bootorder').val(order.join(','));
|
||||
}
|
||||
set_orderlist();
|
||||
$('#b_order').on('change', 'input', set_orderlist);
|
||||
$('#boot_order_up').on('click', function () {
|
||||
$('#b_order label input:checked').each(function () {
|
||||
var label = $(this).parent();
|
||||
var newPos = label.index() - 1;
|
||||
if (newPos > -1) {
|
||||
$('#b_order label').eq(newPos).before(label);
|
||||
}
|
||||
});
|
||||
set_orderlist();
|
||||
});
|
||||
$('#boot_order_down').on('click', function () {
|
||||
var countOptions = $('#b_order label').length;
|
||||
var countSelected = $('#b_order label input:checked').length;
|
||||
$('#b_order label input:checked').each(function () {
|
||||
var label = $(this).parent();
|
||||
var newPos = label.index() + countSelected;
|
||||
if (newPos < countOptions) {
|
||||
$('#b_order label').eq(newPos).after(label);
|
||||
}
|
||||
});
|
||||
set_orderlist();
|
||||
});
|
||||
|
||||
// the Host devices tab's add forms: loaded once, when the tab first opens
|
||||
var hostDevices = document.getElementById("host-devices");
|
||||
if (hostDevices) {
|
||||
document.querySelector('[data-bs-target="#hostdevs"]').addEventListener("shown.bs.tab", function () {
|
||||
if (hostDevices.dataset.loaded) return;
|
||||
hostDevices.dataset.loaded = "true";
|
||||
fetch(hostDevices.dataset.url).then(function (response) {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
return response.text();
|
||||
}).then(function (html) {
|
||||
hostDevices.innerHTML = html;
|
||||
}).catch(function () {
|
||||
hostDevices.textContent = hostDevices.dataset.failed;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// the Migrate tab's disks: loaded for the chosen destination when the tab opens or the choice changes
|
||||
// the form is not sent until the disks of the chosen destination are shown
|
||||
var migratePlan = document.getElementById("migrate-plan");
|
||||
var migrateCompute = document.getElementById("migrate_compute");
|
||||
var migrateLoading = null;
|
||||
function loadMigratePlan() {
|
||||
if (!migrateCompute.value) return;
|
||||
if (migrateLoading) migrateLoading.abort();
|
||||
var loading = migrateLoading = new AbortController();
|
||||
var submit = migratePlan.closest("form").querySelector('button[name="migrate"]');
|
||||
submit.disabled = true;
|
||||
migratePlan.textContent = migratePlan.dataset.checkingDisks;
|
||||
fetch(migratePlan.dataset.url + "?compute_id=" + encodeURIComponent(migrateCompute.value),
|
||||
{signal: loading.signal}).then(function (response) {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
return response.text();
|
||||
}).then(function (html) {
|
||||
migratePlan.innerHTML = html;
|
||||
// a destination that did not answer, or a disk that cannot go there
|
||||
submit.disabled = migratePlan.querySelector("[data-refused]") !== null;
|
||||
}).catch(function () {
|
||||
if (loading.signal.aborted) return; // another destination was chosen
|
||||
migratePlan.textContent = migratePlan.dataset.disksFailed;
|
||||
submit.disabled = false; // the migration checks them again
|
||||
});
|
||||
}
|
||||
// the destinations that cannot be chosen (unreachable, or this host under
|
||||
// another record) are marked first; then the disks of the first one left
|
||||
function loadMigrateTargets() {
|
||||
var submit = migratePlan.closest("form").querySelector('button[name="migrate"]');
|
||||
submit.disabled = true;
|
||||
migrateCompute.disabled = true; // no disks are checked for a host before the hosts are
|
||||
migratePlan.textContent = migratePlan.dataset.checkingHosts;
|
||||
fetch(migratePlan.dataset.targetsUrl).then(function (response) {
|
||||
if (!response.ok) throw new Error(response.statusText);
|
||||
return response.json();
|
||||
}).then(function (refused) {
|
||||
migrateCompute.disabled = false;
|
||||
Array.prototype.forEach.call(migrateCompute.options, function (option) {
|
||||
if (refused[option.value]) {
|
||||
option.disabled = true;
|
||||
option.textContent = refused[option.value]; // it names the host
|
||||
}
|
||||
});
|
||||
var first = Array.prototype.find.call(migrateCompute.options, function (option) { return !option.disabled; });
|
||||
if (!first) {
|
||||
migratePlan.textContent = migratePlan.dataset.noHost;
|
||||
return;
|
||||
}
|
||||
if (migrateCompute.selectedOptions[0].disabled) migrateCompute.value = first.value;
|
||||
loadMigratePlan();
|
||||
}).catch(function () {
|
||||
migratePlan.textContent = migratePlan.dataset.hostsFailed;
|
||||
});
|
||||
}
|
||||
if (migratePlan && migrateCompute) {
|
||||
document.querySelector('[data-bs-target="#migrate"]').addEventListener("shown.bs.tab", function () {
|
||||
if (!migratePlan.dataset.loaded) {
|
||||
migratePlan.dataset.loaded = "true";
|
||||
loadMigrateTargets();
|
||||
}
|
||||
});
|
||||
migrateCompute.addEventListener("change", loadMigratePlan);
|
||||
}
|
||||
|
||||
// the tab a redirect points to; a template opens on its clone form
|
||||
restoreInstanceTab(document, location.hash || summary.dataset.opensOn, show_tab, load_osinfo);
|
||||
// none open (a template has no Power tab): the first section the page has
|
||||
if (!document.querySelector("#navbtn .nav-link.active")) {
|
||||
var first = document.querySelector("#navbtn .nav-link");
|
||||
if (first) { show_tab(first.getAttribute("data-bs-target")); }
|
||||
}
|
||||
})();
|
||||
115
static/js/instance_stats.js
Normal file
115
static/js/instance_stats.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// The VM page's Stats tab (stats_tab.html): its charts are polled while the
|
||||
// tab is shown and made once; superusers' Logs view loads its rows each time
|
||||
// it is shown.
|
||||
(function () {
|
||||
var pane = document.getElementById("graphics");
|
||||
var stats_tab = document.querySelector('#graphics-tab');
|
||||
var stats_poll = null;
|
||||
// the seconds each CPU point averages
|
||||
var cpuWindows = [];
|
||||
// the usage sample of the previous answer: the next one averages since then
|
||||
var statsSample = null;
|
||||
// a reading for the line above a chart: one decimal, a dash where it is missing
|
||||
function reading(value) {
|
||||
return value == null ? "–" : String(Math.round(value * 10) / 10);
|
||||
}
|
||||
stats_tab.addEventListener('hidden.bs.tab', function () {
|
||||
stats_poll.stop();
|
||||
});
|
||||
stats_tab.addEventListener('shown.bs.tab', function () {
|
||||
if (stats_poll !== null) {
|
||||
stats_poll.start();
|
||||
return;
|
||||
}
|
||||
var cpuChart = lineChart(document.getElementById("cpuChart"), "%", ["Usage"], {
|
||||
max: 100,
|
||||
tooltip: function (context, text) {
|
||||
var span = cpuWindows[context.dataIndex];
|
||||
return span ? text + ' (' + pane.dataset.averageOver + ' ' + span + ' s)' : text;
|
||||
},
|
||||
});
|
||||
var memChart = lineChart(document.getElementById("memChart"), "MiB", ["Usage (RSS)"]);
|
||||
var diskChart = {};
|
||||
pane.querySelectorAll("canvas[data-disk]").forEach(function (canvas) {
|
||||
diskChart[canvas.dataset.disk] = lineChart(canvas, "MB/s", ["Read", "Write"]);
|
||||
});
|
||||
// two NICs with one MAC (a misconfiguration) share the first one's chart
|
||||
var netChart = {};
|
||||
pane.querySelectorAll("canvas[data-mac]").forEach(function (canvas) {
|
||||
var mac = canvas.dataset.mac;
|
||||
netChart[mac] = netChart[mac] || lineChart(canvas, "Mbit/s", ["Inbound", "Outbound"]);
|
||||
});
|
||||
|
||||
// the first point at once, then every 5 s like the host graph
|
||||
stats_poll = pollWhileVisible(5000, function () {
|
||||
return $.getJSON(pane.dataset.url, statsSample ? {sample: statsSample} : {})
|
||||
.fail(function () {
|
||||
statsSample = null;
|
||||
});
|
||||
}, function graph_usage(data) {
|
||||
statsSample = data.sample || null;
|
||||
cpuWindows.push(data.window);
|
||||
if (cpuWindows.length > CHART_POINTS) {
|
||||
cpuWindows.shift();
|
||||
}
|
||||
addPoint(cpuChart, data.timeline, [data.cpudata]);
|
||||
document.getElementById("cpuNow").textContent = reading(data.cpudata) + " %";
|
||||
// a VM whose memory could not be read gives total 0: a gap, same axis;
|
||||
// the RSS may pass the balloon size: then the axis grows to it
|
||||
if (data.memdata.total) {
|
||||
var used = Math.round(data.memdata.used / 1024), total = Math.round(data.memdata.total / 1024);
|
||||
memChart.options.scales.y.max = Math.max(total, used);
|
||||
addPoint(memChart, data.timeline, [used]);
|
||||
document.getElementById("memNow").textContent = used + " / " + total + " MiB";
|
||||
} else {
|
||||
addPoint(memChart, data.timeline, [null]);
|
||||
document.getElementById("memNow").textContent = "";
|
||||
}
|
||||
// a disk or NIC added after the page loaded has no chart until a reload,
|
||||
// and one removed keeps its last points
|
||||
function push(chart, id, labels, unit, values) {
|
||||
if (chart) {
|
||||
addPoint(chart, data.timeline, values);
|
||||
document.getElementById(id).textContent =
|
||||
labels[0] + " " + reading(values[0]) + " · " + labels[1] + " " + reading(values[1]) + " " + unit;
|
||||
}
|
||||
}
|
||||
data.blkdata.forEach(function (disk) {
|
||||
push(diskChart[disk.dev], "blk" + disk.dev + "Now", [pane.dataset.read, pane.dataset.write], "MB/s", disk.data);
|
||||
});
|
||||
data.netdata.forEach(function (nic) {
|
||||
push(netChart[nic.mac], "net" + nic.mac + "Now", [pane.dataset.in, pane.dataset.out], "Mbit/s", nic.data);
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// a click or the arrow keys: the rows of the moment each time Logs is shown
|
||||
var logsTab = document.getElementById("logs-tab");
|
||||
if (!logsTab) return;
|
||||
var table = document.getElementById("logs_table");
|
||||
logsTab.addEventListener("shown.bs.tab", function () {
|
||||
var tbody = $(table).children("tbody");
|
||||
function note(text) {
|
||||
tbody.empty().append($("<tr>").append($("<td colspan='4' class='text-body-secondary'>").text(text)));
|
||||
}
|
||||
$.getJSON(table.dataset.url, function (data) {
|
||||
if (!data.length) {
|
||||
note(table.dataset.empty);
|
||||
return;
|
||||
}
|
||||
// Log messages contain user-entered names (snapshots, disks, clones);
|
||||
// insert them as text, never as HTML.
|
||||
tbody.empty();
|
||||
$.each(data, function (id, row) {
|
||||
$("<tr>")
|
||||
.append($("<td>").addClass("text-nowrap").text(row['date']))
|
||||
.append($("<td>").text(row['user']))
|
||||
.append($("<td>").text(row['message']))
|
||||
.append($("<td>").text(row['result']))
|
||||
.appendTo(tbody);
|
||||
});
|
||||
}).fail(function () {
|
||||
note(table.dataset.failed);
|
||||
});
|
||||
});
|
||||
})();
|
||||
13
static/js/instance_status.js
Normal file
13
static/js/instance_status.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// The VM page reloads when the VM's state changes (instance.html, #vm-summary).
|
||||
backgroundJobRunning = false;
|
||||
// not "status": that is window.status, which the browser may not keep
|
||||
var vm_summary = document.getElementById("vm-summary");
|
||||
var vm_status = Number(vm_summary.dataset.status);
|
||||
var status_poll = pollWhileVisible(5000, function () {
|
||||
return $.getJSON(vm_summary.dataset.statusUrl);
|
||||
}, function get_status(data) {
|
||||
// an XML edit not saved yet stays: the reload waits until it is saved or undone
|
||||
if (data['status'] != vm_status && !backgroundJobRunning && !(window.xmlEditUnsaved && window.xmlEditUnsaved())) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
|
@ -13,3 +13,7 @@ function hidePleaseWaitDialog() {
|
|||
pleaseModal.hide();
|
||||
backgroundJobRunning = false;
|
||||
}
|
||||
// a form marked data-please-wait shows the dialog while it is sent
|
||||
document.addEventListener("submit", function (event) {
|
||||
if (event.target.matches("form[data-please-wait]")) showPleaseWaitDialog();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue