1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 19:56:48 +00:00

feat(instances): a clone takes the size asked and starts if asked

A VM made from a template came out at the template's size and shut off,
and was then resized and started by hand. The Clone tab now asks for the
clone's vCPUs and memory, filled in with the VM's, and whether to start
it. After the copy, before the clone is recorded, it is resized with the
Resize tab's own functions and started; a step that fails is a warning
of the job, and the clone stays. The quota counts the larger of the size
asked and the VM's, so a resize that fails leaves the clone at a size it
allowed.

When the job ends the page opens the clone's, not the cloned VM's again:
a template's page, shut off, read as a clone that had not started. An
outcome kept for a warning links to it.
This commit is contained in:
catborise 2026-11-01 22:20:21 +03:00
parent 9aedb5b4d1
commit 8a809443a4
9 changed files with 423 additions and 233 deletions

View file

@ -74,7 +74,8 @@ function jobPanel(element, urls, csrfToken, initialIds) {
var note = job.result && job.result.warning ? '<div class="small mt-2">' + esc(job.result.warning) + "</div>" : "";
var warn = note + (left.length ? '<div class="small mt-2">Files not removed:</div><ul class="list-unstyled small mb-0">'
+ left.map(function (path) { return '<li class="text-break"><code>' + esc(path) + "</code></li>"; }).join("") + "</ul>" : "");
var buttons = '<button type="button" class="btn btn-sm btn-primary" data-refresh="1">Refresh</button>'
var buttons = (opens(job) ? '<a class="btn btn-sm btn-primary" href="' + esc(job.result.open) + '">Open</a>' : "")
+ '<button type="button" class="btn btn-sm btn-primary" data-refresh="1">Refresh</button>'
+ '<button type="button" class="btn btn-sm btn-outline-secondary" data-dismiss="' + esc(job.id)
+ '">Dismiss</button>';
return alertHtml(job.state, header(job.label, job.state, "", buttons) + errorHtml(job) + warn);
@ -114,6 +115,11 @@ function jobPanel(element, urls, csrfToken, initialIds) {
holding = busy;
}
// the page of what the job made, a path of this site
function opens(job) {
return !!(job.result && /^\/[^\/]/.test(job.result.open || ""));
}
// a job that did what it was asked, with nothing more to read: the page shows its effect
function quiet(job) {
var result = job.result || {};
@ -134,7 +140,13 @@ function jobPanel(element, urls, csrfToken, initialIds) {
var others = Object.keys(outcomes).length + Object.keys(pending).length + Object.keys(shown).length;
// the page in use (poll.js): its outcome waits in the panel, with Refresh
if (quiet(job) && !pageInUse() && !others) {
window.location.reload(); // the job is not remembered again: no loop
// a job that made a VM opens its page (result.open); another reloads this one,
// where the job is not remembered again: no loop
if (opens(job)) {
window.location.assign(job.result.open);
} else {
window.location.reload();
}
return;
}
outcomes[id] = job;

View file

@ -19,7 +19,8 @@ function advance(ms) {
const realNow = Date.now;
Date.now = () => 1700000000000 + now;
global.document = { hidden: false, addEventListener() {}, activeElement: null, querySelector: () => null };
global.window = { backgroundJobRunning: false, location: { reload() { window.reloaded = true; } } };
global.window = { backgroundJobRunning: false,
location: { reload() { window.reloaded = true; }, assign(url) { window.opened = url; } } };
// requests settle only when the test says so; callbacks run in registration order
let open = [];
@ -264,6 +265,26 @@ answer("/j/u", { ...running, id: "u", label: "Upload", state: "succeeded" });
assert.ok(window.reloaded, "a job that succeeds without a warning reloads the page: it shows the job's effect");
assert.ok(!el11.innerHTML.includes('<span class="badge bg-success">succeeded</span>'), "not shown as well");
// a job that made a VM opens its page; a warning keeps the outcome, with Open; only a path of this site
open = []; timers = []; window.reloaded = false; window.opened = null;
const elv = { innerHTML: "", addEventListener() {} };
jobPanel(elv, urls, "tok", ["v"]);
advance(0);
answer("/r", empty);
answer("/j/v", { ...running, id: "v", label: "Clone VM", state: "succeeded", result: { open: "/instances/9/" } });
assert.ok(window.opened === "/instances/9/" && !window.reloaded, "opens the clone's page");
for (const [result, shown] of [[{ open: "/instances/9/", warning: "not started" }, true],
[{ open: "//evil.example.com/" }, false], [{ open: "javascript:alert(1)" }, false]]) {
open = []; timers = []; window.reloaded = false; window.opened = null;
const elw = { innerHTML: "", addEventListener() {} };
jobPanel(elw, urls, "tok", ["w"]);
advance(0);
answer("/r", empty);
answer("/j/w", { ...running, id: "w", state: "succeeded", result });
assert.strictEqual(window.opened, null, JSON.stringify(result));
assert.strictEqual(elw.innerHTML.includes('href="/instances/9/">Open</a>'), shown, JSON.stringify(result));
}
// a dialog open or a field in use: the outcome waits in the panel, with Refresh
for (const [label, doc] of [["dialog", { querySelector: (s) => s === ".modal.show" ? {} : null }],
["field", { activeElement: { matches: (s) => s === "input, select, textarea" } }]]) {