Compare commits
10 Commits
8cfafe7399
...
80ac09e040
Author | SHA1 | Date | |
---|---|---|---|
80ac09e040 | |||
199f1b1e99 | |||
29c36cdbf5 | |||
a6a76930f9 | |||
e5a0e92ebb | |||
230fe2c9e4 | |||
7f4bdacef0 | |||
e97f8d5bbc | |||
4806d7f18f | |||
89f0b14c21 |
@ -9,6 +9,7 @@
|
||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/nav.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<script src="scripts/account.js" type="module"></script>
|
||||
<script src="scripts/chart.js" type="module"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
@ -58,10 +59,16 @@
|
||||
<div class="w3-card w3-padding">
|
||||
<h3>Account Details</h3>
|
||||
<p id="username">Username:</p>
|
||||
<p id="pool">Pool:</p>
|
||||
<p id="pool">Pools:</p>
|
||||
<p id="vmid">VMID Range:</p>
|
||||
<p id="nodes">Nodes:</p>
|
||||
</div>
|
||||
<div class="w3-card w3-padding">
|
||||
<div class="flex row nowrap">
|
||||
<h3>Password</h3>
|
||||
<button class="w3-button w3-margin" id="change-password">Change Password</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w3-card w3-padding">
|
||||
<h3>Cluster Resources</h3>
|
||||
<div id="resource-container"></div>
|
||||
|
@ -1,8 +1,6 @@
|
||||
.input-grid {
|
||||
float: left;
|
||||
display: grid;
|
||||
column-gap: 10px;
|
||||
row-gap: 5px;
|
||||
gap: 5px 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
@ -34,6 +32,10 @@ fieldset > *:last-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
fieldset > .input-grid {
|
||||
float: left;
|
||||
}
|
||||
|
||||
body:not(:-moz-handler-blocked) fieldset {
|
||||
display: table-cell;
|
||||
}
|
||||
|
@ -112,3 +112,15 @@ hr, * {
|
||||
.spacer {
|
||||
min-height: 1em;
|
||||
}
|
||||
|
||||
.w3-select, select {
|
||||
padding: 8px;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E:root%7Bcolor:%23fff%7D@media (prefers-color-scheme:light)%7B:root%7Bcolor:%23000%7D%7D%3C/style%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M12.707 14.707a1 1 0 01-1.414 0l-5-5a1 1 0 011.414-1.414L12 12.586l4.293-4.293a1 1 0 111.414 1.414l-5 5z' fill='currentColor'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px top 50%;
|
||||
background-size: 1em auto;
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<script src="scripts/index.js" type="module"></script>
|
||||
<script src="scripts/instance.js" type="module"></script>
|
||||
<script src="modules/wfa.js" type="module"></script>
|
||||
<style>
|
||||
#instance-container > div {
|
||||
border-bottom: 1px solid white;
|
||||
|
328
modules/wfa.js
Normal file
328
modules/wfa.js
Normal file
@ -0,0 +1,328 @@
|
||||
class WavefrontComponent {
|
||||
constructor () {
|
||||
this.lo = [0]; // lo for each wavefront
|
||||
this.hi = [0]; // hi for each wavefront
|
||||
this.W = []; // wavefront diag distance for each wavefront
|
||||
this.A = []; // compact CIGAR for backtrace
|
||||
}
|
||||
|
||||
// get value for wavefront=score, diag=k
|
||||
get_val (score, k) {
|
||||
if (this.W[score] !== undefined && this.W[score][k] !== undefined) {
|
||||
return this.W[score][k];
|
||||
}
|
||||
else {
|
||||
return NaN;
|
||||
}
|
||||
}
|
||||
|
||||
// set value for wavefront=score, diag=k
|
||||
set_val (score, k, val) {
|
||||
if (this.W[score]) {
|
||||
this.W[score][k] = val;
|
||||
}
|
||||
else {
|
||||
this.W[score] = [];
|
||||
this.W[score][k] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// get alignment traceback
|
||||
get_traceback (score, k) {
|
||||
if (this.A[score] !== undefined && this.A[score][k] !== undefined) {
|
||||
return this.A[score][k];
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// set alignment traceback
|
||||
set_traceback (score, k, traceback) {
|
||||
if (this.A[score]) {
|
||||
this.A[score][k] = traceback;
|
||||
}
|
||||
else {
|
||||
this.A[score] = [];
|
||||
this.A[score][k] = traceback;
|
||||
}
|
||||
}
|
||||
|
||||
// get hi for wavefront=score
|
||||
get_hi (score) {
|
||||
const hi = this.hi[score];
|
||||
return isNaN(hi) ? 0 : hi;
|
||||
}
|
||||
|
||||
// set hi for wavefront=score
|
||||
set_hi (score, hi) {
|
||||
this.hi[score] = hi;
|
||||
}
|
||||
|
||||
// get lo for wavefront=score
|
||||
get_lo (score) {
|
||||
const lo = this.lo[score];
|
||||
return isNaN(lo) ? 0 : lo;
|
||||
}
|
||||
|
||||
// set lo for wavefront=score
|
||||
set_lo (score, lo) {
|
||||
this.lo[score] = lo;
|
||||
}
|
||||
|
||||
// string representation of all wavefronts
|
||||
toString () {
|
||||
const traceback_str = ["OI", "EI", "OD", "ED", "SB", "IN", "DL", "EN"];
|
||||
let s = "<";
|
||||
let min_lo = Infinity;
|
||||
let max_hi = -Infinity;
|
||||
// get the min lo and max hi values across all wavefronts
|
||||
for (let i = 0; i < this.W.length; i++) {
|
||||
const lo = this.lo[i];
|
||||
const hi = this.hi[i];
|
||||
if (lo < min_lo) {
|
||||
min_lo = lo;
|
||||
}
|
||||
if (hi > max_hi) {
|
||||
max_hi = hi;
|
||||
}
|
||||
}
|
||||
// print out two headers, one for wavefront and one for traceback
|
||||
for (let k = min_lo; k <= max_hi; k++) {
|
||||
s += FormatNumberLength(k, 2);
|
||||
if (k < max_hi) {
|
||||
s += "|";
|
||||
}
|
||||
}
|
||||
s += ">\t<";
|
||||
for (let k = min_lo; k <= max_hi; k++) {
|
||||
s += FormatNumberLength(k, 2);
|
||||
if (k < max_hi) {
|
||||
s += "|";
|
||||
}
|
||||
}
|
||||
s += ">\n";
|
||||
// for each wavefront
|
||||
for (let i = 0; i < this.W.length; i++) {
|
||||
s += "[";
|
||||
const lo = this.lo[i];
|
||||
const hi = this.hi[i];
|
||||
// print out the wavefront matrix
|
||||
for (let k = min_lo; k <= max_hi; k++) {
|
||||
if (this.W[i] !== undefined && this.W[i][k] !== undefined && !isNaN(this.W[i][k])) {
|
||||
s += FormatNumberLength(this.W[i][k], 2);
|
||||
}
|
||||
else if (k < lo || k > hi) {
|
||||
s += "--";
|
||||
}
|
||||
else {
|
||||
s += " ";
|
||||
}
|
||||
if (k < max_hi) {
|
||||
s += "|";
|
||||
}
|
||||
}
|
||||
s += "]\t[";
|
||||
// print out the traceback matrix
|
||||
for (let k = min_lo; k <= max_hi; k++) {
|
||||
if (this.A[i] !== undefined && this.A[i][k] !== undefined) {
|
||||
s += traceback_str[this.A[i][k].toString()];
|
||||
}
|
||||
else if (k < lo || k > hi) {
|
||||
s += "--";
|
||||
}
|
||||
else {
|
||||
s += " ";
|
||||
}
|
||||
if (k < max_hi) {
|
||||
s += "|";
|
||||
}
|
||||
}
|
||||
s += "]\n";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
const traceback = {
|
||||
OpenIns: 0,
|
||||
ExtdIns: 1,
|
||||
OpenDel: 2,
|
||||
ExtdDel: 3,
|
||||
Sub: 4,
|
||||
Ins: 5,
|
||||
Del: 6,
|
||||
End: 7
|
||||
};
|
||||
|
||||
function FormatNumberLength (num, length) {
|
||||
let r = "" + num;
|
||||
while (r.length < length) {
|
||||
r = " " + r;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function min (args) {
|
||||
args.forEach((el, idx, arr) => {
|
||||
arr[idx] = isNaN(el) ? Infinity : el;
|
||||
});
|
||||
const min = Math.min.apply(Math, args);
|
||||
return min === Infinity ? NaN : min;
|
||||
}
|
||||
|
||||
function max (args) {
|
||||
args.forEach((el, idx, arr) => {
|
||||
arr[idx] = isNaN(el) ? -Infinity : el;
|
||||
});
|
||||
const max = Math.max.apply(Math, args);
|
||||
return max === -Infinity ? NaN : max;
|
||||
}
|
||||
|
||||
function argmax (args) {
|
||||
const val = max(args);
|
||||
return args.indexOf(val);
|
||||
}
|
||||
|
||||
export default function wf_align (s1, s2, penalties) {
|
||||
const n = s1.length;
|
||||
const m = s2.length;
|
||||
const A_k = m - n;
|
||||
const A_offset = m;
|
||||
let score = 0;
|
||||
const M = new WavefrontComponent();
|
||||
M.set_val(0, 0, 0);
|
||||
M.set_hi(0, 0);
|
||||
M.set_lo(0, 0);
|
||||
M.set_traceback(0, 0, traceback.End);
|
||||
const I = new WavefrontComponent();
|
||||
const D = new WavefrontComponent();
|
||||
while (true) {
|
||||
wf_extend(M, s1, n, s2, m, score);
|
||||
if (M.get_val(score, A_k) >= A_offset) {
|
||||
break;
|
||||
}
|
||||
score++;
|
||||
wf_next(M, I, D, score, penalties);
|
||||
}
|
||||
return wf_backtrace(M, I, D, score, penalties, A_k, A_offset);
|
||||
}
|
||||
|
||||
function wf_extend (M, s1, n, s2, m, score) {
|
||||
const lo = M.get_lo(score);
|
||||
const hi = M.get_hi(score);
|
||||
for (let k = lo; k <= hi; k++) {
|
||||
let v = M.get_val(score, k) - k;
|
||||
let h = M.get_val(score, k);
|
||||
if (isNaN(v) || isNaN(h)) {
|
||||
continue;
|
||||
}
|
||||
while (s1[v] === s2[h]) {
|
||||
M.set_val(score, k, M.get_val(score, k) + 1);
|
||||
v++;
|
||||
h++;
|
||||
if (v > n || h > m) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function wf_next (M, I, D, score, penalties) {
|
||||
const x = penalties.x;
|
||||
const o = penalties.o;
|
||||
const e = penalties.e;
|
||||
const lo = min([M.get_lo(score - x), M.get_lo(score - o - e), I.get_lo(score - e), D.get_lo(score - e)]) - 1;
|
||||
const hi = max([M.get_hi(score - x), M.get_hi(score - o - e), I.get_hi(score - e), D.get_hi(score - e)]) + 1;
|
||||
M.set_hi(score, hi);
|
||||
I.set_hi(score, hi);
|
||||
D.set_hi(score, hi);
|
||||
M.set_lo(score, lo);
|
||||
I.set_lo(score, lo);
|
||||
D.set_lo(score, lo);
|
||||
for (let k = lo; k <= hi; k++) {
|
||||
I.set_val(score, k, max([
|
||||
M.get_val(score - o - e, k - 1),
|
||||
I.get_val(score - e, k - 1)
|
||||
]) + 1);
|
||||
I.set_traceback(score, k, [traceback.OpenIns, traceback.ExtdIns][argmax([
|
||||
M.get_val(score - o - e, k - 1),
|
||||
I.get_val(score - e, k - 1)
|
||||
])]);
|
||||
D.set_val(score, k, max([
|
||||
M.get_val(score - o - e, k + 1),
|
||||
D.get_val(score - e, k + 1)
|
||||
]));
|
||||
D.set_traceback(score, k, [traceback.OpenDel, traceback.ExtdDel][argmax([
|
||||
M.get_val(score - o - e, k + 1),
|
||||
D.get_val(score - e, k + 1)
|
||||
])]);
|
||||
M.set_val(score, k, max([
|
||||
M.get_val(score - x, k) + 1,
|
||||
I.get_val(score, k),
|
||||
D.get_val(score, k)
|
||||
]));
|
||||
M.set_traceback(score, k, [traceback.Sub, traceback.Ins, traceback.Del][argmax([
|
||||
M.get_val(score - x, k) + 1,
|
||||
I.get_val(score, k),
|
||||
D.get_val(score, k)
|
||||
])]);
|
||||
}
|
||||
}
|
||||
|
||||
function wf_backtrace (M, I, D, score, penalties, A_k) {
|
||||
const traceback_CIGAR = ["I", "I", "D", "D", "X", "", "", ""];
|
||||
const x = penalties.x;
|
||||
const o = penalties.o;
|
||||
const e = penalties.e;
|
||||
let CIGAR_rev = ""; // reversed CIGAR
|
||||
let tb_s = score; // traceback score
|
||||
let tb_k = A_k; // traceback diag k
|
||||
let current_traceback = M.get_traceback(tb_s, tb_k);
|
||||
let done = false;
|
||||
while (!done) {
|
||||
CIGAR_rev += traceback_CIGAR[current_traceback];
|
||||
switch (current_traceback) {
|
||||
case traceback.OpenIns:
|
||||
tb_s = tb_s - o - e;
|
||||
tb_k = tb_k - 1;
|
||||
current_traceback = M.get_traceback(tb_s, tb_k);
|
||||
break;
|
||||
case traceback.ExtdIns:
|
||||
tb_s = tb_s - e;
|
||||
tb_k = tb_k - 1;
|
||||
current_traceback = I.get_traceback(tb_s, tb_k);
|
||||
break;
|
||||
case traceback.OpenDel:
|
||||
tb_s = tb_s - o - e;
|
||||
tb_k = tb_k + 1;
|
||||
current_traceback = M.get_traceback(tb_s, tb_k);
|
||||
break;
|
||||
case traceback.ExtdDel:
|
||||
tb_s = tb_s - e;
|
||||
tb_k = tb_k + 1;
|
||||
current_traceback = D.get_traceback(tb_s, tb_k);
|
||||
break;
|
||||
case traceback.Sub:
|
||||
tb_s = tb_s - x;
|
||||
// tb_k = tb_k;
|
||||
current_traceback = M.get_traceback(tb_s, tb_k);
|
||||
break;
|
||||
case traceback.Ins:
|
||||
// tb_s = tb_s;
|
||||
// tb_k = tb_k;
|
||||
current_traceback = I.get_traceback(tb_s, tb_k);
|
||||
break;
|
||||
case traceback.Del:
|
||||
// tb_s = tb_s;
|
||||
// tb_k = tb_k;
|
||||
current_traceback = D.get_traceback(tb_s, tb_k);
|
||||
break;
|
||||
case traceback.End:
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const CIGAR = Array.from(CIGAR_rev).reverse().join("");
|
||||
return { CIGAR, score };
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
"description": "Front-end for ProxmoxAAS",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "html-validator --continue; stylelint --formatter verbose --fix **/*.css; DEBUG=eslint:cli-engine eslint --fix ."
|
||||
"lint": "html-validator --continue; stylelint --formatter verbose --fix **/*.css; DEBUG=eslint:cli-engine eslint --fix scripts/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.43.0",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { dialog } from "./dialog.js";
|
||||
import { requestAPI, goToPage, getCookie, setTitleAndHeader } from "./utils.js";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", init);
|
||||
@ -28,20 +29,20 @@ async function init () {
|
||||
|
||||
let resources = requestAPI("/user/dynamic/resources");
|
||||
let meta = requestAPI("/global/config/resources");
|
||||
let instances = requestAPI("/user/config/cluster");
|
||||
let nodes = requestAPI("/user/config/nodes");
|
||||
let userCluster = requestAPI("/user/config/cluster");
|
||||
|
||||
resources = await resources;
|
||||
meta = await meta;
|
||||
instances = await instances;
|
||||
nodes = await nodes;
|
||||
userCluster = await userCluster;
|
||||
|
||||
document.querySelector("#username").innerText = `Username: ${getCookie("username")}`;
|
||||
document.querySelector("#pool").innerText = `Pool: ${instances.pool}`;
|
||||
document.querySelector("#vmid").innerText = `VMID Range: ${instances.vmid.min} - ${instances.vmid.max}`;
|
||||
document.querySelector("#nodes").innerText = `Nodes: ${nodes.toString()}`;
|
||||
document.querySelector("#pool").innerText = `Pools: ${Object.keys(userCluster.pools).toString()}`;
|
||||
document.querySelector("#vmid").innerText = `VMID Range: ${userCluster.vmid.min} - ${userCluster.vmid.max}`;
|
||||
document.querySelector("#nodes").innerText = `Nodes: ${Object.keys(userCluster.nodes).toString()}`;
|
||||
|
||||
populateResources("#resource-container", meta, resources);
|
||||
|
||||
document.querySelector("#change-password").addEventListener("click", handlePasswordChangeForm);
|
||||
}
|
||||
|
||||
function populateResources (containerID, meta, resources) {
|
||||
@ -50,12 +51,12 @@ function populateResources (containerID, meta, resources) {
|
||||
Object.keys(meta).forEach((resourceType) => {
|
||||
if (meta[resourceType].display) {
|
||||
if (meta[resourceType].type === "list") {
|
||||
resources[resourceType].forEach((listResource) => {
|
||||
resources[resourceType].total.forEach((listResource) => {
|
||||
createResourceUsageChart(container, listResource.name, listResource.avail, listResource.used, listResource.max, null);
|
||||
});
|
||||
}
|
||||
else {
|
||||
createResourceUsageChart(container, meta[resourceType].name, resources[resourceType].avail, resources[resourceType].used, resources[resourceType].max, meta[resourceType]);
|
||||
createResourceUsageChart(container, meta[resourceType].name, resources[resourceType].total.avail, resources[resourceType].total.used, resources[resourceType].total.max, meta[resourceType]);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -117,3 +118,32 @@ function parseNumber (value, unitData) {
|
||||
return `${value} ${unit}`;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePasswordChangeForm () {
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="new-password">New Password</label>
|
||||
<input class="w3-input w3-border" id="new-password" name="new-password" type="password"required>
|
||||
<label for="confirm-password">Confirm Password</label>
|
||||
<input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" required>
|
||||
</form>
|
||||
`;
|
||||
const d = dialog("Change Password", body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
const result = await requestAPI("/auth/password", "POST", { password: form.get("new-password") });
|
||||
if (result.status !== 200) {
|
||||
alert(result.error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const password = d.querySelector("#new-password");
|
||||
const confirmPassword = d.querySelector("#confirm-password");
|
||||
|
||||
function validatePassword () {
|
||||
confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : "");
|
||||
}
|
||||
|
||||
password.addEventListener("change", validatePassword);
|
||||
confirmPassword.addEventListener("keyup", validatePassword);
|
||||
}
|
||||
|
@ -54,8 +54,10 @@ async function populateResources () {
|
||||
const global = await requestAPI("/global/config/resources");
|
||||
const user = await requestAPI("/user/config/resources");
|
||||
let options = [];
|
||||
if (global.cpu.whitelist) {
|
||||
user.cpu.forEach((userType) => {
|
||||
const globalCPU = global.cpu;
|
||||
const userCPU = node in user.cpu.nodes ? user.cpu.nodes[node] : user.cpu.global;
|
||||
if (globalCPU.whitelist) {
|
||||
userCPU.forEach((userType) => {
|
||||
options.push(userType.name);
|
||||
});
|
||||
options = options.sort((a, b) => {
|
||||
@ -65,7 +67,7 @@ async function populateResources () {
|
||||
else {
|
||||
const supported = await requestPVE(`/nodes/${node}/capabilities/qemu/cpu`);
|
||||
supported.data.forEach((supportedType) => {
|
||||
if (!user.cpu.some((userType) => supportedType.name === userType.name)) {
|
||||
if (!userCPU.some((userType) => supportedType.name === userType.name)) {
|
||||
options.push(supportedType.name);
|
||||
}
|
||||
});
|
||||
@ -233,7 +235,7 @@ function addDiskLine (fieldset, busPrefix, busName, device, diskDetails) {
|
||||
async function handleDiskDetach () {
|
||||
const disk = this.dataset.disk;
|
||||
const header = `Detach ${disk}`;
|
||||
const body = `<p>Are you sure you want to detach disk</p><p>${disk}</p>`;
|
||||
const body = `<p>Are you sure you want to detach disk ${disk}</p>`;
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
|
||||
@ -250,7 +252,12 @@ async function handleDiskDetach () {
|
||||
|
||||
async function handleDiskAttach () {
|
||||
const header = `Attach ${this.dataset.disk}`;
|
||||
const body = `<label for="device">${type === "qemu" ? "SATA" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" required></input>`;
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="device">${type === "qemu" ? "SATA" : "MP"}</label>
|
||||
<input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" required></input>
|
||||
</form>
|
||||
`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@ -274,7 +281,12 @@ async function handleDiskAttach () {
|
||||
|
||||
async function handleDiskResize () {
|
||||
const header = `Resize ${this.dataset.disk}`;
|
||||
const body = "<label for=\"size-increment\">Size Increment (GiB)</label><input class=\"w3-input w3-border\" name=\"size-increment\" id=\"size-increment\" type=\"number\" min=\"0\" max=\"131072\"></input>";
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="size-increment">Size Increment (GiB)</label>
|
||||
<input class="w3-input w3-border" name="size-increment" id="size-increment" type="number" min="0" max="131072"></input>
|
||||
</form>
|
||||
`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@ -310,8 +322,10 @@ async function handleDiskMove () {
|
||||
const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select"><option hidden disabled selected value></option>${options}</select>`;
|
||||
|
||||
const body = `
|
||||
${select}
|
||||
<label for="delete-check">Delete Source</label><input class="w3-input w3-border" name="delete-check" id="delete-check" type="checkbox" checked required>
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
${select}
|
||||
<label for="delete-check">Delete Source</label><input class="w3-input w3-border" name="delete-check" id="delete-check" type="checkbox" checked required>
|
||||
</form>
|
||||
`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
@ -337,7 +351,7 @@ async function handleDiskMove () {
|
||||
async function handleDiskDelete () {
|
||||
const disk = this.dataset.disk;
|
||||
const header = `Delete ${disk}`;
|
||||
const body = `<p>Are you sure you want to <strong>delete</strong> disk</p><p>${disk}</p>`;
|
||||
const body = `<p>Are you sure you want to <strong>delete</strong> disk${disk}</p>`;
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
|
||||
@ -367,9 +381,11 @@ async function handleDiskAdd () {
|
||||
const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required><option hidden disabled selected value></option>${options}</select>`;
|
||||
|
||||
const body = `
|
||||
<label for="device">${type === "qemu" ? "SATA" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" value="0" required></input>
|
||||
${select}
|
||||
<label for="size">Size (GiB)</label><input class="w3-input w3-border" name="size" id="size" type="number" min="0" max="131072" required></input>
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="device">${type === "qemu" ? "SATA" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" value="0" required></input>
|
||||
${select}
|
||||
<label for="size">Size (GiB)</label><input class="w3-input w3-border" name="size" id="size" type="number" min="0" max="131072" required></input>
|
||||
</form>
|
||||
`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
@ -393,23 +409,15 @@ async function handleDiskAdd () {
|
||||
}
|
||||
|
||||
async function handleCDAdd () {
|
||||
const content = "iso";
|
||||
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
||||
const isos = await requestAPI("/user/vm-isos", "GET");
|
||||
|
||||
const header = "Add a CDROM";
|
||||
|
||||
let storageOptions = "";
|
||||
storage.data.forEach((element) => {
|
||||
if (element.content.includes(content)) {
|
||||
storageOptions += `<option value="${element.storage}">${element.storage}</option>"`;
|
||||
}
|
||||
});
|
||||
const storageSelect = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required><option hidden disabled selected value></option>${storageOptions}</select>`;
|
||||
|
||||
const body = `
|
||||
<label for="device">IDE</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="3" required></input>
|
||||
${storageSelect}
|
||||
<label for="iso-select">Image</label><select class="w3-select w3-border" name="iso-select" id="iso-select" required></select>
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="device">IDE</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="3" required></input>
|
||||
<label for="iso-select">Image</label><select class="w3-select w3-border" name="iso-select" id="iso-select" required></select>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const d = dialog(header, body, async (result, form) => {
|
||||
@ -428,17 +436,12 @@ async function handleCDAdd () {
|
||||
}
|
||||
});
|
||||
|
||||
d.querySelector("#storage-select").addEventListener("change", async () => {
|
||||
const storage = document.querySelector("#storage-select").value;
|
||||
const ISOSelect = document.querySelector("#iso-select");
|
||||
ISOSelect.innerHTML = "<option hidden disabled selected value></option>";
|
||||
const isos = await requestPVE(`/nodes/${node}/storage/${storage}/content`, "GET", { content });
|
||||
isos.data.forEach((element) => {
|
||||
if (element.content.includes(content)) {
|
||||
ISOSelect.append(new Option(element.volid.replace(`${storage}:${content}/`, ""), element.volid));
|
||||
}
|
||||
});
|
||||
});
|
||||
const isoSelect = d.querySelector("#iso-select");
|
||||
|
||||
for (const iso of isos) {
|
||||
isoSelect.append(new Option(iso.name, iso.volid));
|
||||
}
|
||||
isoSelect.selectedIndex = -1;
|
||||
}
|
||||
|
||||
async function populateNetworks () {
|
||||
@ -509,7 +512,11 @@ async function handleNetworkConfig () {
|
||||
const netID = this.dataset.network;
|
||||
const netDetails = this.dataset.values;
|
||||
const header = `Edit net${netID}`;
|
||||
const body = "<label for=\"rate\">Rate Limit (MB/s)</label><input type=\"number\" id=\"rate\" name=\"rate\" class=\"w3-input w3-border\">";
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
|
||||
</form>
|
||||
`;
|
||||
|
||||
const d = dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@ -523,7 +530,8 @@ async function handleNetworkConfig () {
|
||||
}
|
||||
await getConfig();
|
||||
populateNetworks();
|
||||
updateBootLine(`boot-net${netID}`, { id: `net${netID}`, prefix: "net", value: config.data[`net${netID}`] });
|
||||
const id = `net${netID}`;
|
||||
updateBootLine(`boot-net${netID}`, { id, prefix: "net", value: id, detail: config.data[`net${netID}`] });
|
||||
}
|
||||
});
|
||||
|
||||
@ -551,10 +559,15 @@ async function handleNetworkDelete () {
|
||||
|
||||
async function handleNetworkAdd () {
|
||||
const header = "Create Network Interface";
|
||||
let body = "<label for=\"netid\">Interface ID</label><input type=\"number\" id=\"netid\" name=\"netid\" class=\"w3-input w3-border\"><label for=\"rate\">Rate Limit (MB/s)</label><input type=\"number\" id=\"rate\" name=\"rate\" class=\"w3-input w3-border\">";
|
||||
let body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="netid">Interface ID</label><input type="number" id="netid" name="netid" class="w3-input w3-border">
|
||||
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
|
||||
`;
|
||||
if (type === "lxc") {
|
||||
body += "<label for=\"name\">Interface Name</label><input type=\"text\" id=\"name\" name=\"name\" class=\"w3-input w3-border\"></input>";
|
||||
}
|
||||
body += "</form>";
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@ -648,7 +661,11 @@ async function handleDeviceConfig () {
|
||||
const deviceDetails = this.dataset.values;
|
||||
const deviceName = this.dataset.name;
|
||||
const header = `Edit Expansion Card ${deviceID}`;
|
||||
const body = "<label for=\"device\">Device</label><select id=\"device\" name=\"device\" required></select><label for=\"pcie\">PCI-Express</label><input type=\"checkbox\" id=\"pcie\" name=\"pcie\" class=\"w3-input w3-border\">";
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="device">Device</label><select id="device" name="device" required></select><label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
|
||||
</form>
|
||||
`;
|
||||
|
||||
const d = dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@ -694,7 +711,11 @@ async function handleDeviceDelete () {
|
||||
|
||||
async function handleDeviceAdd () {
|
||||
const header = "Add Expansion Card";
|
||||
const body = "<label for=\"device\">Device</label><select id=\"device\" name=\"device\" required></select><label for=\"pcie\">PCI-Express</label><input type=\"checkbox\" id=\"pcie\" name=\"pcie\" class=\"w3-input w3-border\">";
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="device">Device</label><select id="device" name="device" required></select><label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
|
||||
</form>
|
||||
`;
|
||||
|
||||
const d = dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@ -798,11 +819,11 @@ function updateBootLine (id, newData) {
|
||||
const enabled = document.querySelector("#enabled");
|
||||
const disabled = document.querySelector("#disabled");
|
||||
let element = null;
|
||||
if (enabled.getItemByID(id)) {
|
||||
element = enabled.getItemByID(id);
|
||||
if (enabled.querySelector(`#${id}`)) {
|
||||
element = enabled.querySelector(`#${id}`);
|
||||
}
|
||||
if (disabled.getItemByID(id)) {
|
||||
element = disabled.getItemByID(id);
|
||||
if (disabled.querySelector(`#${id}`)) {
|
||||
element = disabled.querySelector(`#${id}`);
|
||||
}
|
||||
if (element) {
|
||||
const container = element.container;
|
||||
|
@ -1,20 +1,32 @@
|
||||
export function dialog (header, body, callback = async (result, form) => { }) {
|
||||
export function dialog (header, body, onclose = async (result, form) => { }) {
|
||||
const dialog = document.createElement("dialog");
|
||||
dialog.innerHTML = `
|
||||
<p class="w3-large" id="prompt" style="text-align: center;"></p>
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form"></form>
|
||||
<div id="body"></div>
|
||||
<div class="w3-center w3-container">
|
||||
<button value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
</div>
|
||||
`;
|
||||
dialog.className = "w3-container w3-card w3-border-0";
|
||||
dialog.querySelector("#prompt").innerText = header;
|
||||
dialog.querySelector("form").innerHTML = body;
|
||||
dialog.querySelector("#body").innerHTML = body;
|
||||
dialog.addEventListener("close", async () => {
|
||||
await callback(dialog.returnValue, new FormData(dialog.querySelector("form")));
|
||||
const formElem = dialog.querySelector("form");
|
||||
const formData = formElem ? new FormData(formElem) : null;
|
||||
await onclose(dialog.returnValue, formData);
|
||||
dialog.parentElement.removeChild(dialog);
|
||||
});
|
||||
if (!dialog.querySelector("form")) {
|
||||
dialog.querySelector("#confirm").addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
dialog.close(e.target.value);
|
||||
});
|
||||
dialog.querySelector("#cancel").addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
dialog.close(e.target.value);
|
||||
});
|
||||
}
|
||||
document.body.append(dialog);
|
||||
dialog.showModal();
|
||||
return dialog;
|
||||
|
155
scripts/index.js
155
scripts/index.js
@ -1,6 +1,7 @@
|
||||
import { requestPVE, requestAPI, goToPage, setTitleAndHeader } from "./utils.js";
|
||||
import { alert, dialog } from "./dialog.js";
|
||||
import { setupClientSync } from "./clientsync.js";
|
||||
import wf_align from "../modules/wfa.js";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
@ -38,6 +39,11 @@ async function getInstances () {
|
||||
}
|
||||
|
||||
async function populateInstances () {
|
||||
let searchCriteria = localStorage.getItem("search-criteria");
|
||||
if (!searchCriteria) {
|
||||
searchCriteria = "fuzzy";
|
||||
localStorage.setItem("search-criteria", "fuzzy");
|
||||
}
|
||||
const searchQuery = document.querySelector("#search").value || null;
|
||||
let criteria;
|
||||
if (!searchQuery) {
|
||||
@ -45,7 +51,7 @@ async function populateInstances () {
|
||||
return (a.vmid > b.vmid) ? 1 : -1;
|
||||
};
|
||||
}
|
||||
else {
|
||||
else if (searchCriteria === "exact") {
|
||||
criteria = (a, b) => {
|
||||
const aInc = a.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const bInc = b.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
@ -63,6 +69,27 @@ async function populateInstances () {
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (searchCriteria === "fuzzy") {
|
||||
const penalties = {
|
||||
m: 0,
|
||||
x: 1,
|
||||
o: 1,
|
||||
e: 1
|
||||
};
|
||||
criteria = (a, b) => {
|
||||
// lower is better
|
||||
const aAlign = wf_align(a.name.toLowerCase(), searchQuery.toLowerCase(), penalties);
|
||||
const aScore = aAlign.score / a.name.length;
|
||||
const bAlign = wf_align(b.name.toLowerCase(), searchQuery.toLowerCase(), penalties);
|
||||
const bScore = bAlign.score / b.name.length;
|
||||
if (aScore === bScore) {
|
||||
return a.vmid > b.vmid ? 1 : -1;
|
||||
}
|
||||
else {
|
||||
return aScore - bScore;
|
||||
}
|
||||
};
|
||||
}
|
||||
instances.sort(criteria);
|
||||
const instanceContainer = document.querySelector("#instance-container");
|
||||
instanceContainer.innerHTML = "";
|
||||
@ -78,42 +105,47 @@ async function handleInstanceAdd () {
|
||||
const header = "Create New Instance";
|
||||
|
||||
const body = `
|
||||
<label for="type">Instance Type</label>
|
||||
<select class="w3-select w3-border" name="type" id="type" required>
|
||||
<option value="lxc">Container</option>
|
||||
<option value="qemu">Virtual Machine</option>
|
||||
</select>
|
||||
<label for="node">Node</label>
|
||||
<select class="w3-select w3-border" name="node" id="node" required></select>
|
||||
<label for="name">Name</label>
|
||||
<input class="w3-input w3-border" name="name" id="name" required></input>
|
||||
<label for="vmid">ID</label>
|
||||
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required></input>
|
||||
<label for="cores">Cores (Threads)</label>
|
||||
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required></input>
|
||||
<label for="memory">Memory (MiB)</label>
|
||||
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16", step="1" required></input>
|
||||
<p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p>
|
||||
<label class="container-specific none" for="swap">Swap (MiB)</label>
|
||||
<input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled></input>
|
||||
<label class="container-specific none" for="template-storage">Template Storage</label>
|
||||
<select class="w3-select w3-border container-specific none" name="template-storage" id="template-storage" required disabled></select>
|
||||
<label class="container-specific none" for="template-image">Template Image</label>
|
||||
<select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select>
|
||||
<label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label>
|
||||
<select class="w3-select w3-border container-specific none" name="rootfs-storage" id="rootfs-storage" required disabled></select>
|
||||
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
|
||||
<input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled></input>
|
||||
<label class="container-specific none" for="password">Password</label>
|
||||
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled></input>
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="type">Instance Type</label>
|
||||
<select class="w3-select w3-border" name="type" id="type" required>
|
||||
<option value="lxc">Container</option>
|
||||
<option value="qemu">Virtual Machine</option>
|
||||
</select>
|
||||
<label for="node">Node</label>
|
||||
<select class="w3-select w3-border" name="node" id="node" required></select>
|
||||
<label for="name">Name</label>
|
||||
<input class="w3-input w3-border" name="name" id="name" required></input>
|
||||
<label for="vmid">ID</label>
|
||||
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required></input>
|
||||
<label for="pool">Pool</label>
|
||||
<select class="w3-select w3-border" name="pool" id="pool" required></select>
|
||||
<label for="cores">Cores (Threads)</label>
|
||||
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required></input>
|
||||
<label for="memory">Memory (MiB)</label>
|
||||
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16", step="1" required></input>
|
||||
<p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p>
|
||||
<label class="container-specific none" for="swap">Swap (MiB)</label>
|
||||
<input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled></input>
|
||||
<label class="container-specific none" for="template-image">Template Image</label>
|
||||
<select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select>
|
||||
<label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label>
|
||||
<select class="w3-select w3-border container-specific none" name="rootfs-storage" id="rootfs-storage" required disabled></select>
|
||||
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
|
||||
<input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled></input>
|
||||
<label class="container-specific none" for="password">Password</label>
|
||||
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled></input>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const templates = await requestAPI("/user/ct-templates", "GET");
|
||||
|
||||
const d = dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
const body = {
|
||||
name: form.get("name"),
|
||||
cores: form.get("cores"),
|
||||
memory: form.get("memory")
|
||||
memory: form.get("memory"),
|
||||
pool: form.get("pool")
|
||||
};
|
||||
if (form.get("type") === "lxc") {
|
||||
body.swap = form.get("swap");
|
||||
@ -153,55 +185,64 @@ async function handleInstanceAdd () {
|
||||
}
|
||||
});
|
||||
|
||||
const templateContent = "iso";
|
||||
const templateStorage = d.querySelector("#template-storage");
|
||||
templateStorage.selectedIndex = -1;
|
||||
|
||||
const rootfsContent = "rootdir";
|
||||
const rootfsStorage = d.querySelector("#rootfs-storage");
|
||||
rootfsStorage.selectedIndex = -1;
|
||||
|
||||
const userResources = await requestAPI("/user/dynamic/resources", "GET");
|
||||
const userCluster = await requestAPI("/user/config/cluster", "GET");
|
||||
|
||||
const nodeSelect = d.querySelector("#node");
|
||||
const clusterNodes = await requestPVE("/nodes", "GET");
|
||||
const allowedNodes = await requestAPI("/user/config/nodes", "GET");
|
||||
const allowedNodes = Object.keys(userCluster.nodes);
|
||||
clusterNodes.data.forEach((element) => {
|
||||
if (element.status === "online" && allowedNodes.includes(element.node)) {
|
||||
nodeSelect.add(new Option(element.node));
|
||||
}
|
||||
});
|
||||
nodeSelect.selectedIndex = -1;
|
||||
nodeSelect.addEventListener("change", async () => { // change template and rootfs storage based on node
|
||||
nodeSelect.addEventListener("change", async () => { // change rootfs storage based on node
|
||||
const node = nodeSelect.value;
|
||||
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
||||
storage.data.forEach((element) => {
|
||||
if (element.content.includes(templateContent)) {
|
||||
templateStorage.add(new Option(element.storage));
|
||||
}
|
||||
if (element.content.includes(rootfsContent)) {
|
||||
rootfsStorage.add(new Option(element.storage));
|
||||
}
|
||||
});
|
||||
templateStorage.selectedIndex = -1;
|
||||
rootfsStorage.selectedIndex = -1;
|
||||
|
||||
// set core and memory min/max depending on node selected
|
||||
if (node in userResources.cores.nodes) {
|
||||
d.querySelector("#cores").max = userResources.cores.nodes[node].avail;
|
||||
}
|
||||
else {
|
||||
d.querySelector("#cores").max = userResources.cores.global.avail;
|
||||
}
|
||||
|
||||
if (node in userResources.memory.nodes) {
|
||||
d.querySelector("#memory").max = userResources.memory.nodes[node].avail;
|
||||
}
|
||||
else {
|
||||
d.querySelector("#memory").max = userResources.memory.global.avail;
|
||||
}
|
||||
});
|
||||
|
||||
const templateImage = d.querySelector("#template-image"); // populate templateImage depending on selected image storage
|
||||
templateStorage.addEventListener("change", async () => {
|
||||
templateImage.innerHTML = "";
|
||||
const content = "vztmpl";
|
||||
const images = await requestPVE(`/nodes/${nodeSelect.value}/storage/${templateStorage.value}/content`, "GET");
|
||||
images.data.forEach((element) => {
|
||||
if (element.content.includes(content)) {
|
||||
templateImage.append(new Option(element.volid.replace(`${templateStorage.value}:${content}/`, ""), element.volid));
|
||||
}
|
||||
});
|
||||
templateImage.selectedIndex = -1;
|
||||
});
|
||||
|
||||
const userResources = await requestAPI("/user/dynamic/resources", "GET");
|
||||
const userCluster = await requestAPI("/user/config/cluster", "GET");
|
||||
d.querySelector("#cores").max = userResources.cores.avail;
|
||||
d.querySelector("#memory").max = userResources.memory.avail;
|
||||
// set vmid min/max
|
||||
d.querySelector("#vmid").min = userCluster.vmid.min;
|
||||
d.querySelector("#vmid").max = userCluster.vmid.max;
|
||||
|
||||
// add user pools to selector
|
||||
const poolSelect = d.querySelector("#pool");
|
||||
const userPools = Object.keys(userCluster.pools);
|
||||
userPools.forEach((element) => {
|
||||
poolSelect.add(new Option(element));
|
||||
});
|
||||
poolSelect.selectedIndex = -1;
|
||||
|
||||
// add template images to selector
|
||||
const templateImage = d.querySelector("#template-image"); // populate templateImage depending on selected image storage
|
||||
for (const template of templates) {
|
||||
templateImage.append(new Option(template.name, template.volid));
|
||||
}
|
||||
templateImage.selectedIndex = -1;
|
||||
}
|
||||
|
@ -162,7 +162,7 @@ class InstanceCard extends HTMLElement {
|
||||
async handlePowerButton () {
|
||||
if (!this.actionLock) {
|
||||
const header = `${this.status === "running" ? "Stop" : "Start"} VM ${this.vmid}`;
|
||||
const body = `<p>Are you sure you want to ${this.status === "running" ? "stop" : "start"} VM</p><p>${this.vmid}</p>`;
|
||||
const body = `<p>Are you sure you want to ${this.status === "running" ? "stop" : "start"} VM ${this.vmid}</p>`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@ -219,7 +219,7 @@ class InstanceCard extends HTMLElement {
|
||||
handleDeleteButton () {
|
||||
if (!this.actionLock && this.status === "stopped") {
|
||||
const header = `Delete VM ${this.vmid}`;
|
||||
const body = `<p>Are you sure you want to <strong>delete</strong> VM </p><p>${this.vmid}</p>`;
|
||||
const body = `<p>Are you sure you want to <strong>delete</strong> VM ${this.vmid}</p>`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
@ -233,7 +233,9 @@ class InstanceCard extends HTMLElement {
|
||||
|
||||
const result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE");
|
||||
if (result.status === 200) {
|
||||
this.parentElement.removeChild(this);
|
||||
if (this.parentElement) {
|
||||
this.parentElement.removeChild(this);
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert(result.error);
|
||||
|
@ -12,6 +12,10 @@ function init () {
|
||||
if (rate) {
|
||||
document.querySelector("#sync-rate").value = rate;
|
||||
}
|
||||
const search = localStorage.getItem("search-criteria");
|
||||
if (search) {
|
||||
document.querySelector(`#search-${search}`).checked = true;
|
||||
}
|
||||
document.querySelector("#settings").addEventListener("submit", handleSaveSettings, false);
|
||||
}
|
||||
|
||||
@ -20,5 +24,6 @@ function handleSaveSettings (event) {
|
||||
const form = new FormData(document.querySelector("#settings"));
|
||||
localStorage.setItem("sync-scheme", form.get("sync-scheme"));
|
||||
localStorage.setItem("sync-rate", form.get("sync-rate"));
|
||||
localStorage.setItem("search-criteria", form.get("search-criteria"));
|
||||
window.location.reload();
|
||||
}
|
||||
|
@ -51,10 +51,10 @@
|
||||
<h3>Synchronization Settings</h3>
|
||||
<fieldset>
|
||||
<legend>App Sync Method</legend>
|
||||
<label><input class="w3-radio" type="radio" id="sync-always" name="sync-scheme" value="always" required><span>Always Sync</span></label>
|
||||
<label><input class="w3-radio" type="radio" id="sync-always" name="sync-scheme" value="always" required>Always Sync</label>
|
||||
<p>App will always periodically synchronize with Proxmox. High resource usage.</p>
|
||||
<label><input class="w3-radio" type="radio" id="sync-hash" name="sync-scheme" value="hash" required>Check For Sync</label>
|
||||
<p>App will periodically synchronize only if there have been change. Medium resource usage.</p>
|
||||
<p>App will periodically check for updates and synchronize only if needed. Medium resource usage.</p>
|
||||
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
|
||||
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
|
||||
</fieldset>
|
||||
@ -65,6 +65,16 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="w3-card w3-padding">
|
||||
<h3>Search Settings</h3>
|
||||
<fieldset>
|
||||
<legend>Instance Search Criteria</legend>
|
||||
<label><input class="w3-radio" type="radio" id="search-exact" name="search-criteria" value="exact" required>Exact Match</label>
|
||||
<p>Sorts by exact query match in instance name.</p>
|
||||
<label><input class="w3-radio" type="radio" id="search-fuzzy" name="search-criteria" value="fuzzy" required>Fuzzy Match</label>
|
||||
<p>Sorts by best matching to worst matching.</p>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="w3-container w3-center" id="form-actions">
|
||||
<button class="w3-button w3-margin" id="save" type="submit">SAVE</button>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user