move admin related pages to new project
This commit is contained in:
parent
9b3d9767e1
commit
eebb86dd41
@ -50,7 +50,6 @@
|
||||
<a href="index.html">Instances</a>
|
||||
<a href="account.html" aria-current="page">Account</a>
|
||||
<a href="settings.html">Settings</a>
|
||||
<a id="admin-link" aria-disabled="true" class="none">Admin</a>
|
||||
<a href="login.html">Logout</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
66
admin.html
66
admin.html
@ -1,66 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>proxmox - dashboard</title>
|
||||
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="modules/w3.css">
|
||||
<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">
|
||||
<script src="scripts/admin.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>proxmox</h1>
|
||||
<label for="navtoggle">☰</label>
|
||||
<input type="checkbox" id="navtoggle">
|
||||
<nav id="navigation">
|
||||
<a href="index.html">Instances</a>
|
||||
<a href="account.html">Account</a>
|
||||
<a href="settings.html">Settings</a>
|
||||
<a id="admin-link" href="admin.html" aria-current="page">Admin</a>
|
||||
<a href="login.html">Logout</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h2>Admin</h2>
|
||||
<section class="w3-card w3-padding">
|
||||
<div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;">
|
||||
<h3>Users</h3>
|
||||
<button type="button" id="user-add" class="w3-button" aria-label="Create New User">
|
||||
<span class="large" style="margin: 0;">Create User</span>
|
||||
<svg class="small" style="height: 1lh; width: 1lh;" role="img" aria-label="Create New User"><use href="images/actions/user/add.svg#symb"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w3-row" style="border-bottom: 1px solid;">
|
||||
<p class="w3-col l4 m4 s6">User</p>
|
||||
<p class="w3-col l4 m4 w3-hide-small">Groups</p>
|
||||
<p class="w3-col l2 w3-hide-medium w3-hide-small">Admin</p>
|
||||
<p class="w3-col l2 m4 s6">Actions</p>
|
||||
</div>
|
||||
<div id="users-container"></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="w3-card w3-padding">
|
||||
<div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;">
|
||||
<h3>Groups</h3>
|
||||
<button type="button" id="group-add" class="w3-button" aria-label="Create New Group">
|
||||
<span class="large" style="margin: 0;">Create Group</span>
|
||||
<svg class="small" style="height: 1lh; width: 1lh;" role="img" aria-label="Create New Group"><use href="images/actions/group/add.svg#symb"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="w3-row" style="border-bottom: 1px solid;">
|
||||
<p class="w3-col l4 m4 s6">Group</p>
|
||||
<p class="w3-col l6 m4 w3-hide-small">Members</p>
|
||||
<p class="w3-col l2 m4 s6">Actions</p>
|
||||
</div>
|
||||
<div id="groups-container"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
@ -40,7 +40,6 @@
|
||||
<a href="index.html" aria-current="page">Instances</a>
|
||||
<a href="account.html">Account</a>
|
||||
<a href="settings.html">Settings</a>
|
||||
<a id="admin-link" aria-disabled="true" class="none">Admin</a>
|
||||
<a href="login.html">Logout</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
@ -10,7 +10,7 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/nav.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<script src="scripts/config.js" type="module"></script>
|
||||
<script src="scripts/instance.js" type="module"></script>
|
||||
<script src="scripts/draggable.js" type="module"></script>
|
||||
<script src="modules/Sortable.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
@ -32,7 +32,6 @@
|
||||
<a href="index.html" aria-current="page">Instances</a>
|
||||
<a href="account.html">Account</a>
|
||||
<a href="settings.html">Settings</a>
|
||||
<a id="admin-link" aria-disabled="true" class="none">Admin</a>
|
||||
<a href="login.html">Logout</a>
|
||||
</nav>
|
||||
</header>
|
195
scripts/admin.js
195
scripts/admin.js
@ -1,195 +0,0 @@
|
||||
import { setTitleAndHeader, setAppearance, requestAPI, goToPage, isEmpty } from "./utils.js";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
async function init () {
|
||||
setAppearance();
|
||||
setTitleAndHeader();
|
||||
|
||||
const cookie = document.cookie;
|
||||
if (cookie === "") {
|
||||
goToPage("login.html");
|
||||
}
|
||||
|
||||
document.querySelector("#user-add").addEventListener("click", handleUserAdd);
|
||||
document.querySelector("#group-add").addEventListener("click", handleGroupAdd);
|
||||
|
||||
await getUsers();
|
||||
await getGroups();
|
||||
}
|
||||
|
||||
async function getUsers () {
|
||||
const users = (await requestAPI("/access/users")).users;
|
||||
const usersContainer = document.querySelector("#users-container");
|
||||
for (const user of Object.keys(users)) {
|
||||
const newUserCard = document.createElement("user-card");
|
||||
users[user].username = user;
|
||||
newUserCard.data = users[user];
|
||||
usersContainer.append(newUserCard);
|
||||
}
|
||||
}
|
||||
|
||||
async function getGroups () {
|
||||
const groups = (await requestAPI("/access/groups")).groups;
|
||||
const groupsContainer = document.querySelector("#groups-container");
|
||||
for (const group of Object.keys(groups)) {
|
||||
const newGroupCard = document.createElement("group-card");
|
||||
groups[group].groupname = group;
|
||||
newGroupCard.data = groups[group];
|
||||
groupsContainer.append(newGroupCard);
|
||||
}
|
||||
}
|
||||
|
||||
class UserCard extends HTMLElement {
|
||||
constructor () {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<link rel="stylesheet" href="modules/w3.css">
|
||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<div class="w3-row" style="margin-top: 1em; margin-bottom: 1em;">
|
||||
<p class="w3-col l4 m4 s6" id="user-name">
|
||||
<p class="w3-col l4 m4 w3-hide-small" id="user-groups">
|
||||
<p class="w3-col l2 w3-hide-medium w3-hide-small" id="user-admin">
|
||||
<div class="w3-col l2 m4 s6 flex row nowrap" style="height: 1lh;">
|
||||
<svg class="clickable" id="config-btn" role="img" aria-label="Edit User"><use href="images/actions/user/config.svg#symb"></use></svg>
|
||||
<svg class="clickable" id="delete-btn" role="img" aria-label="Delete User"><use href="images/actions/delete-active.svg#symb"></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const configButton = this.shadowRoot.querySelector("#config-btn");
|
||||
configButton.onclick = this.handleConfigButton.bind(this);
|
||||
|
||||
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
|
||||
deleteButton.onclick = this.handleDeleteButton.bind(this);
|
||||
}
|
||||
|
||||
get data () {
|
||||
return {
|
||||
username: this.username,
|
||||
groups: this.groups,
|
||||
admin: this.admin
|
||||
};
|
||||
}
|
||||
|
||||
set data (data) {
|
||||
this.username = data.username;
|
||||
this.groups = this.#getGroupsFromAttribute(data.attributes.memberOf);
|
||||
this.admin = data.cluster.admin;
|
||||
this.update();
|
||||
}
|
||||
|
||||
#getGroupsFromAttribute (attribute) {
|
||||
return Array.from(attribute, (e) => e.split("cn=")[1].split(",")[0]);
|
||||
}
|
||||
|
||||
update () {
|
||||
const nameParagraph = this.shadowRoot.querySelector("#user-name");
|
||||
nameParagraph.innerText = this.username;
|
||||
|
||||
const groupsParagraph = this.shadowRoot.querySelector("#user-groups");
|
||||
if (isEmpty(this.groups)) {
|
||||
groupsParagraph.innerHTML = " ";
|
||||
}
|
||||
else {
|
||||
groupsParagraph.innerText = this.groups.toString();
|
||||
}
|
||||
|
||||
const adminParagraph = this.shadowRoot.querySelector("#user-admin");
|
||||
adminParagraph.innerText = this.admin;
|
||||
}
|
||||
|
||||
handleConfigButton () {
|
||||
goToPage("user.html", { username: this.username });
|
||||
}
|
||||
|
||||
handleDeleteButton () {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
class GroupCard extends HTMLElement {
|
||||
constructor () {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<link rel="stylesheet" href="modules/w3.css">
|
||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="w3-row" style="margin-top: 1em; margin-bottom: 1em;">
|
||||
<p class="w3-col l4 m4 s6" id="group-name">
|
||||
<p class="w3-col l6 m4 w3-hide-small" id="group-members">
|
||||
<div class="w3-col l2 m4 s6 flex row nowrap" style="height: 1lh;">
|
||||
<svg class="clickable" id="config-btn" role="img" aria-label="Edit User"><use href="images/actions/user/config.svg#symb"></use></svg>
|
||||
<svg class="clickable" id="delete-btn" role="img" aria-label="Delete User"><use href="images/actions/delete-active.svg#symb"></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const configButton = this.shadowRoot.querySelector("#config-btn");
|
||||
configButton.onclick = this.handleConfigButton.bind(this);
|
||||
|
||||
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
|
||||
deleteButton.onclick = this.handleDeleteButton.bind(this);
|
||||
}
|
||||
|
||||
get data () {
|
||||
return {
|
||||
groupname: this.groupname,
|
||||
members: this.members
|
||||
};
|
||||
}
|
||||
|
||||
set data (data) {
|
||||
this.groupname = data.groupname;
|
||||
this.members = this.#getMembersFromAttribute(data.attributes.member);
|
||||
this.update();
|
||||
}
|
||||
|
||||
#getMembersFromAttribute (attribute) {
|
||||
const filteredGroups = attribute.filter(e => e !== "");
|
||||
return Array.from(filteredGroups, (e) => e.split("uid=")[1].split(",")[0]);
|
||||
}
|
||||
|
||||
update () {
|
||||
const nameParagraph = this.shadowRoot.querySelector("#group-name");
|
||||
nameParagraph.innerText = this.groupname;
|
||||
|
||||
const membersParagraph = this.shadowRoot.querySelector("#group-members");
|
||||
membersParagraph.innerText = `${this.members.toString()}`;
|
||||
}
|
||||
|
||||
handleConfigButton () {
|
||||
// TODO
|
||||
}
|
||||
|
||||
handleDeleteButton () {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("user-card", UserCard);
|
||||
customElements.define("group-card", GroupCard);
|
||||
|
||||
function handleUserAdd () {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function handleGroupAdd () {
|
||||
// TODO
|
||||
}
|
@ -65,7 +65,7 @@ class DraggableContainer extends HTMLElement {
|
||||
get value () {
|
||||
const value = [];
|
||||
this.content.childNodes.forEach((element) => {
|
||||
if (element.value) {
|
||||
if (element.value !== null) {
|
||||
value.push(element.value);
|
||||
}
|
||||
});
|
||||
@ -74,6 +74,7 @@ class DraggableContainer extends HTMLElement {
|
||||
}
|
||||
|
||||
class DraggableItem extends HTMLElement {
|
||||
#value = null;
|
||||
uuid = null;
|
||||
constructor () {
|
||||
super();
|
||||
@ -103,6 +104,14 @@ class DraggableItem extends HTMLElement {
|
||||
set innerHTML (innerHTML) {
|
||||
this.content.innerHTML = innerHTML;
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.#value;
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.#value = value;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("draggable-container", DraggableContainer);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { requestPVE, requestAPI, goToPage, setTitleAndHeader, setAppearance, getSearchSettings, goToURL, instancesConfig, nodesConfig, setSVGSrc, setSVGAlt } from "./utils.js";
|
||||
import { alert, dialog } from "./dialog.js";
|
||||
import { setupClientSync } from "./clientsync.js";
|
||||
import wfAlign from "../modules/wfa.js";
|
||||
import wfaInit from "../modules/wfa.js";
|
||||
import { PVE } from "../vars.js";
|
||||
|
||||
class InstanceCard extends HTMLElement {
|
||||
@ -212,7 +212,7 @@ class InstanceCard extends HTMLElement {
|
||||
|
||||
handleConfigButton () {
|
||||
if (!this.actionLock && this.status === "stopped") { // if the action lock is false, and the node is stopped, then navigate to the config page with the node info in the search query
|
||||
goToPage("config.html", { node: this.node.name, type: this.type, vmid: this.vmid });
|
||||
goToPage("instance.html", { node: this.node.name, type: this.type, vmid: this.vmid });
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,6 +271,8 @@ async function init () {
|
||||
goToPage("login.html");
|
||||
}
|
||||
|
||||
await wfaInit("./modules/wfa.wasm");
|
||||
|
||||
document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd);
|
||||
document.querySelector("#vm-search").addEventListener("input", populateInstances);
|
||||
|
||||
@ -329,7 +331,7 @@ async function populateInstances () {
|
||||
};
|
||||
criteria = (item, query) => {
|
||||
// lower is better
|
||||
const { score, CIGAR } = wfAlign(query, item, penalties, true);
|
||||
const { score, CIGAR } = global.wfAlign(query, item, penalties, true);
|
||||
return { score: score / item.length, alignment: CIGAR };
|
||||
};
|
||||
}
|
||||
|
@ -105,12 +105,12 @@ async function populateResources () {
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
}
|
||||
addResourceLine(resourcesConfigPage, field, "cpu", { value: config.data.cpu, options });
|
||||
addResourceLine(resourcesConfigPage.cpu, field, { value: config.data.cpu, options });
|
||||
}
|
||||
addResourceLine(resourcesConfigPage, field, "cores", { value: config.data.cores, min: 1, max: 8192 });
|
||||
addResourceLine(resourcesConfigPage, field, "memory", { value: config.data.memory, min: 16, step: 1 });
|
||||
addResourceLine(resourcesConfigPage.cores, field, { value: config.data.cores, min: 1, max: 8192 });
|
||||
addResourceLine(resourcesConfigPage.memory, field, { value: config.data.memory, min: 16, step: 1 });
|
||||
if (type === "lxc") {
|
||||
addResourceLine(resourcesConfigPage, field, "swap", { value: config.data.swap, min: 0, step: 1 });
|
||||
addResourceLine(resourcesConfigPage.swap, field, { value: config.data.swap, min: 0, step: 1 });
|
||||
}
|
||||
}
|
||||
|
496
scripts/user.js
496
scripts/user.js
@ -1,496 +0,0 @@
|
||||
import { goToPage, getURIData, setTitleAndHeader, setAppearance, requestAPI, resourcesConfig, mergeDeep, addResourceLine, setSVGAlt, setSVGSrc } from "./utils.js";
|
||||
import { alert, dialog } from "./dialog.js";
|
||||
|
||||
window.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
let username;
|
||||
let userData;
|
||||
let allGroups;
|
||||
let allNodes;
|
||||
let allPools;
|
||||
let clusterResourceConfig;
|
||||
|
||||
const resourceInputTypes = { // input types for each resource for config page
|
||||
cpu: {
|
||||
element: "interactive-list",
|
||||
align: "start"
|
||||
},
|
||||
cores: {
|
||||
element: "input",
|
||||
attributes: {
|
||||
type: "number"
|
||||
}
|
||||
},
|
||||
memory: {
|
||||
element: "input",
|
||||
attributes: {
|
||||
type: "number"
|
||||
}
|
||||
},
|
||||
swap: {
|
||||
element: "input",
|
||||
attributes: {
|
||||
type: "number"
|
||||
}
|
||||
},
|
||||
network: {
|
||||
element: "input",
|
||||
attributes: {
|
||||
type: "number"
|
||||
}
|
||||
},
|
||||
pci: {
|
||||
element: "interactive-list",
|
||||
align: "start"
|
||||
}
|
||||
};
|
||||
|
||||
class InteractiveList extends HTMLElement {
|
||||
#name;
|
||||
#addText;
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<link rel="stylesheet" href="modules/w3.css">
|
||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
#wrapper {
|
||||
border: 1px dotted var(--main-text-color);
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
<div class="w3-center" id="wrapper">
|
||||
<div id="container"></div>
|
||||
<svg id="add-btn" class="clickable" tabindex="0" role="button"><use></use></svg>
|
||||
</div>
|
||||
`;
|
||||
this.addBtn = this.shadowRoot.querySelector("#add-btn");
|
||||
this.addBtn.onclick = this.#handleAdd.bind(this);
|
||||
this.container = this.shadowRoot.querySelector("#container");
|
||||
setSVGSrc(this.addBtn, "images/common/add.svg");
|
||||
setSVGAlt(this.addBtn, "Add Item");
|
||||
}
|
||||
|
||||
get name () {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
set name (name) {
|
||||
this.#name = name;
|
||||
}
|
||||
|
||||
get addText () {
|
||||
return this.#addText;
|
||||
}
|
||||
|
||||
set addText (addText) {
|
||||
this.#addText = addText;
|
||||
}
|
||||
|
||||
get value () {
|
||||
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
for (const item of value) {
|
||||
this.#addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
#addItem (item) {
|
||||
const itemElem = document.createElement("interactive-list-match-item");
|
||||
itemElem.name = item.name;
|
||||
itemElem.match = item.match;
|
||||
itemElem.max = item.max;
|
||||
this.container.appendChild(itemElem);
|
||||
}
|
||||
|
||||
#handleAdd () {
|
||||
const header = `Add New ${this.#name} Rule`;
|
||||
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="name">Rule Name</label>
|
||||
<input class="w3-input w3-border" name="name" id="name" type="text" required>
|
||||
<label for="match">Matching Pattern</label>
|
||||
<input class="w3-input w3-border" name="match" id="match" type="text" required>
|
||||
<label for="max">Max Resource</label>
|
||||
<input class="w3-input w3-border" name="max" id="max" type="number" required>
|
||||
</form>
|
||||
`;
|
||||
|
||||
dialog(header, body, (result, form) => {
|
||||
if (result === "confirm") {
|
||||
const newItem = {
|
||||
name: form.get("name"),
|
||||
match: form.get("match"),
|
||||
max: form.get("max")
|
||||
};
|
||||
this.#addItem(newItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class InteractiveListMatchItem extends HTMLElement {
|
||||
#name;
|
||||
#match;
|
||||
#max;
|
||||
|
||||
#nameElem;
|
||||
#matchElem;
|
||||
#maxElem;
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<link rel="stylesheet" href="modules/w3.css">
|
||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
#container {
|
||||
text-align: left;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
svg {
|
||||
margin-top: calc(-0.5em + 0.5lh);
|
||||
}
|
||||
</style>
|
||||
<div id="container" class="w3-row">
|
||||
<p class="w3-col l2 w3-hide-medium w3-hide-small"><span id="name"></span></p>
|
||||
<p class="w3-col l7 m9 s11">match="<span id="match"></span>"</p>
|
||||
<p class="w3-col l2 m2 w3-hide-small">max=<span id="max"></span></p>
|
||||
<svg id="delete-btn" class="w3-col l1 m1 s1 clickable" tabindex="0" role="button"><use></use></svg>
|
||||
</div>
|
||||
`;
|
||||
this.#nameElem = this.shadowRoot.querySelector("#name");
|
||||
this.#matchElem = this.shadowRoot.querySelector("#match");
|
||||
this.#maxElem = this.shadowRoot.querySelector("#max");
|
||||
|
||||
this.deleteBtn = this.shadowRoot.querySelector("#delete-btn");
|
||||
this.deleteBtn.onclick = this.#handleDelete.bind(this);
|
||||
setSVGSrc(this.deleteBtn, "images/actions/delete-active.svg");
|
||||
setSVGAlt(this.deleteBtn, "Delete Item");
|
||||
}
|
||||
|
||||
#update () {
|
||||
this.#nameElem.innerText = this.#name;
|
||||
this.#matchElem.innerText = this.#match;
|
||||
this.#maxElem.innerText = this.#max;
|
||||
}
|
||||
|
||||
get name () {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
set name (name) {
|
||||
this.#name = name;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
get match () {
|
||||
return this.#match;
|
||||
}
|
||||
|
||||
set match (match) {
|
||||
this.#match = match;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
get max () {
|
||||
return this.#max;
|
||||
}
|
||||
|
||||
set max (max) {
|
||||
this.#max = max;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
#handleDelete () {
|
||||
const header = `Delete ${this.name}`;
|
||||
const body = `<p>Are you sure you want to <strong>delete</strong> ${this.name}</p>`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
if (this.parentElement) {
|
||||
this.parentElement.removeChild(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("interactive-list", InteractiveList);
|
||||
customElements.define("interactive-list-match-item", InteractiveListMatchItem);
|
||||
|
||||
const resourcesConfigPage = mergeDeep({}, resourcesConfig, resourceInputTypes);
|
||||
|
||||
async function init () {
|
||||
setAppearance();
|
||||
setTitleAndHeader();
|
||||
const cookie = document.cookie;
|
||||
if (cookie === "") {
|
||||
goToPage("login.html");
|
||||
}
|
||||
|
||||
const uriData = getURIData();
|
||||
username = uriData.username;
|
||||
|
||||
document.querySelector("#name").innerHTML = document.querySelector("#name").innerHTML.replace("%{username}", username);
|
||||
|
||||
await getUser();
|
||||
await populateGroups();
|
||||
await populateResources();
|
||||
await populateCluster();
|
||||
|
||||
clusterResourceConfig = (await requestAPI("/global/config/resources")).resources;
|
||||
|
||||
document.querySelector("#exit").addEventListener("click", handleFormExit);
|
||||
}
|
||||
|
||||
async function getUser () {
|
||||
userData = (await requestAPI(`/access/users/${username}`)).user;
|
||||
allGroups = (await requestAPI("/access/groups")).groups;
|
||||
allNodes = (await requestAPI("/cluster/nodes")).nodes;
|
||||
allPools = (await requestAPI("/cluster/pools")).pools;
|
||||
}
|
||||
|
||||
async function populateGroups () {
|
||||
const groupsDisabled = document.querySelector("#groups-disabled");
|
||||
const groupsEnabled = document.querySelector("#groups-enabled");
|
||||
// for each group in cluster
|
||||
for (const groupName of Object.keys(allGroups)) {
|
||||
const group = allGroups[groupName];
|
||||
const item = document.createElement("draggable-item");
|
||||
item.data = group;
|
||||
item.innerHTML = `
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; column-gap: 10px; align-items: center;">
|
||||
<svg id="drag" role="application" aria-label="drag icon"><title>drag icon</title><use href="images/actions/drag.svg#symb"></use></svg>
|
||||
<p style="margin: 0px;">${group.attributes.cn}</p>
|
||||
</div>
|
||||
`;
|
||||
// if user in group
|
||||
if (userData.attributes.memberOf.indexOf(group.dn) !== -1) {
|
||||
groupsEnabled.append(item);
|
||||
}
|
||||
// user is not in group
|
||||
else {
|
||||
groupsDisabled.append(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function populateResources () {
|
||||
const field = document.querySelector("#resources");
|
||||
for (const resourceName of Object.keys(userData.resources)) {
|
||||
const resource = userData.resources[resourceName];
|
||||
if (resourcesConfigPage[resourceName]) {
|
||||
const resourceConfig = resourcesConfigPage[resourceName];
|
||||
let resourceLine;
|
||||
|
||||
if (resourceName === "cpu" || resourceName === "pci") {
|
||||
resourceLine = addResourceLine(resourcesConfigPage, field, resourceName, { value: resource.global }, "(Global)");
|
||||
}
|
||||
else {
|
||||
resourceLine = addResourceLine(resourcesConfigPage, field, resourceName, { value: resource.global.max }, "(Global)");
|
||||
}
|
||||
|
||||
postPopulateResourceLine(field, resourceName, "global", resourceConfig, resourceLine);
|
||||
|
||||
for (const nodeSpecificName of Object.keys(resource.nodes)) { // for each node specific, add a line with the node name as a prefix
|
||||
if (resourceName === "cpu" || resourceName === "pci") {
|
||||
resourceLine = addResourceLine(resourcesConfigPage, field, resourceName, { value: resource.nodes[nodeSpecificName] }, `(${nodeSpecificName})`);
|
||||
}
|
||||
else {
|
||||
resourceLine = addResourceLine(resourcesConfigPage, field, resourceName, { value: resource.nodes[nodeSpecificName].max }, `(${nodeSpecificName})`);
|
||||
}
|
||||
|
||||
postPopulateResourceLine(field, resourceName, nodeSpecificName, resourceConfig, resourceLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
document.querySelector("#resource-add").addEventListener("click", handleResourceAdd);
|
||||
}
|
||||
|
||||
function postPopulateResourceLine (field, resourceName, resourceScope, resourceConfig, resourceLine) {
|
||||
const deleteBtn = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
deleteBtn.classList.add("clickable");
|
||||
setSVGSrc(deleteBtn, "images/actions/delete-active.svg");
|
||||
setSVGAlt(deleteBtn, "Delete Rule");
|
||||
field.appendChild(deleteBtn);
|
||||
|
||||
resourceLine.field = field;
|
||||
resourceLine.deleteBtn = deleteBtn;
|
||||
deleteBtn.onclick = handleResourceDelete.bind(resourceLine);
|
||||
|
||||
if (resourceConfig.align && resourceConfig.align === "start") {
|
||||
resourceLine.icon.style.alignSelf = "start";
|
||||
resourceLine.icon.style.marginTop = "calc(8px + (0.5lh - 0.5em))";
|
||||
resourceLine.label.style.alignSelf = "start";
|
||||
}
|
||||
|
||||
resourceLine.resourceName = resourceName;
|
||||
resourceLine.resourceScope = resourceScope;
|
||||
}
|
||||
|
||||
async function handleResourceAdd () {
|
||||
const header = "Add New Resource Constraint";
|
||||
const body = `
|
||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||
<label for="name">PVE Resource Name</label>
|
||||
<select class="w3-select w3-border" name="name" id="name" required></select>
|
||||
<label for="scope">Constraint Scope</label>
|
||||
<select class="w3-select w3-border" name="scope" id="scope" required>
|
||||
<option value="global">Global</option>
|
||||
</select>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const d = dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
const name = form.get("name");
|
||||
const type = clusterResourceConfig[name].type;
|
||||
const scope = form.get("scope");
|
||||
|
||||
console.log(name, type, scope);
|
||||
|
||||
// check if the resource name is not in the cluster config resources
|
||||
if (!clusterResourceConfig[name]) {
|
||||
alert(`${name} is not an allowed resource name`);
|
||||
}
|
||||
// check if a global scope rule already exists in the user's resource config
|
||||
else if (scope === "global" && userData.resources[name] && userData.resources[name].global) {
|
||||
alert(`${name} (${scope}) is already a rule`);
|
||||
}
|
||||
// check if node specific rule already exists in the user's resource config
|
||||
else if (scope !== "global" && userData.resources[name] && userData.resources[name].nodes[scope]) {
|
||||
alert(`${name} (${scope}) is already a rule`);
|
||||
}
|
||||
// no existing rule exists, add a new resource rule line and add a the rule to userData
|
||||
else {
|
||||
// if the rule does not exist at all, add a temporary filler to mark that a new rule has been created
|
||||
if (!userData.resources[name]) {
|
||||
userData.resources[name] = {
|
||||
global: null,
|
||||
node: {}
|
||||
};
|
||||
}
|
||||
|
||||
const field = document.querySelector("#resources");
|
||||
let resourceLine;
|
||||
|
||||
if (scope === "global" && type === "numeric") {
|
||||
userData.resources[name].global = { max: 0 };
|
||||
resourceLine = addResourceLine(resourcesConfigPage, field, name, { value: userData.resources[name].global.max }, "(Global)");
|
||||
}
|
||||
else if (scope === "global" && type === "list") {
|
||||
userData.resources[name].global = [];
|
||||
resourceLine = addResourceLine(resourcesConfigPage, field, name, { value: userData.resources[name].global }, "(Global)");
|
||||
}
|
||||
else if (scope !== "global" && type === "numeric") {
|
||||
userData.resources[name].nodes[scope] = { max: 0 };
|
||||
resourceLine = addResourceLine(resourcesConfigPage, field, name, { value: userData.resources[name].nodes[scope].max }, `(${scope})`);
|
||||
}
|
||||
else if (scope !== "global" && type === "list") {
|
||||
userData.resources[name].nodes[scope] = [];
|
||||
resourceLine = addResourceLine(resourcesConfigPage, field, name, { value: userData.resources[name].nodes[scope] }, `(${scope})`);
|
||||
}
|
||||
|
||||
postPopulateResourceLine(field, name, scope, resourcesConfigPage[name], resourceLine);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const nameSelect = d.querySelector("#name");
|
||||
for (const resourceName of Object.keys(clusterResourceConfig)) {
|
||||
nameSelect.add(new Option(resourceName, resourceName));
|
||||
}
|
||||
|
||||
const scopeSelect = d.querySelector("#scope");
|
||||
for (const node of allNodes) {
|
||||
scopeSelect.add(new Option(node, node));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResourceDelete () {
|
||||
const header = `Delete Resource Constraint ${this.label.innerText}`;
|
||||
const body = `<p>Are you sure you want to <strong>delete</strong> VM ${this.label.innerText}</p>`;
|
||||
|
||||
dialog(header, body, async (result, form) => {
|
||||
if (result === "confirm") {
|
||||
this.icon.parentElement.removeChild(this.icon);
|
||||
this.label.parentElement.removeChild(this.label);
|
||||
this.element.parentElement.removeChild(this.element);
|
||||
this.unit.parentElement.removeChild(this.unit);
|
||||
this.deleteBtn.parentElement.removeChild(this.deleteBtn);
|
||||
|
||||
if (this.resourceScope === "global") {
|
||||
userData.resources[this.resourceName].global = false;
|
||||
}
|
||||
else {
|
||||
userData.resources[this.resourceName].nodes[this.resourceScope] = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function populateCluster () {
|
||||
const nodesEnabled = document.querySelector("#nodes-enabled");
|
||||
const nodesDisabled = document.querySelector("#nodes-disabled");
|
||||
const poolsEnabled = document.querySelector("#pools-enabled");
|
||||
const poolsDisabled = document.querySelector("#pools-disabled");
|
||||
|
||||
for (const node of allNodes) { // for each node of all cluster nodes
|
||||
const item = document.createElement("draggable-item");
|
||||
item.data = node;
|
||||
item.innerHTML = `
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; column-gap: 10px; align-items: center;">
|
||||
<svg id="drag" role="application" aria-label="drag icon"><title>drag icon</title><use href="images/actions/drag.svg#symb"></use></svg>
|
||||
<p style="margin: 0px;">${node}</p>
|
||||
</div>
|
||||
`;
|
||||
if (userData.cluster.nodes[node] === true) {
|
||||
nodesEnabled.append(item);
|
||||
}
|
||||
else {
|
||||
nodesDisabled.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
for (const pool of allPools) { // for each pool of all cluster pools
|
||||
const item = document.createElement("draggable-item");
|
||||
item.data = pool;
|
||||
item.innerHTML = `
|
||||
<div style="display: grid; grid-template-columns: auto 1fr; column-gap: 10px; align-items: center;">
|
||||
<svg id="drag" role="application" aria-label="drag icon"><title>drag icon</title><use href="images/actions/drag.svg#symb"></use></svg>
|
||||
<p style="margin: 0px;">${pool}</p>
|
||||
</div>
|
||||
`;
|
||||
if (userData.cluster.pools[pool] === true) {
|
||||
poolsEnabled.append(item);
|
||||
}
|
||||
else {
|
||||
poolsDisabled.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
const vmidMin = document.querySelector("#vmid-min");
|
||||
const vmidMax = document.querySelector("#vmid-max");
|
||||
|
||||
vmidMin.value = userData.cluster.vmid.min;
|
||||
vmidMax.value = userData.cluster.vmid.max;
|
||||
|
||||
const adminCheckbox = document.querySelector("#admin");
|
||||
adminCheckbox.checked = userData.cluster.admin === true;
|
||||
}
|
||||
|
||||
async function handleFormExit () {
|
||||
// TODO
|
||||
}
|
@ -300,15 +300,6 @@ export function getURIData () {
|
||||
export async function setTitleAndHeader () {
|
||||
document.title = `${organization} - dashboard`;
|
||||
document.querySelector("h1").innerText = organization;
|
||||
if (getCookie("auth") === "1") {
|
||||
const userIsAdmin = (await requestAPI("/user/config/cluster")).admin;
|
||||
if (userIsAdmin) {
|
||||
const adminNavLink = document.querySelector("#navigation #admin-link");
|
||||
adminNavLink.href = "admin.html";
|
||||
adminNavLink.classList.remove("none");
|
||||
adminNavLink.ariaDisabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const settingsDefault = {
|
||||
@ -427,8 +418,7 @@ export function isEmpty (obj) {
|
||||
}
|
||||
}
|
||||
|
||||
export function addResourceLine (config, field, resourceType, attributesOverride, labelPrefix = null) {
|
||||
const resourceConfig = config[resourceType];
|
||||
export function addResourceLine (resourceConfig, field, attributesOverride, labelPrefix = null) {
|
||||
const iconHref = resourceConfig.icon;
|
||||
const elementType = resourceConfig.element;
|
||||
const labelText = labelPrefix ? `${labelPrefix} ${resourceConfig.name}` : resourceConfig.name;
|
||||
|
@ -41,7 +41,6 @@
|
||||
<a href="index.html">Instances</a>
|
||||
<a href="account.html">Account</a>
|
||||
<a href="settings.html" aria-current="page">Settings</a>
|
||||
<a id="admin-link" aria-disabled="true" class="none">Admin</a>
|
||||
<a href="login.html">Logout</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
102
user.html
102
user.html
@ -1,102 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>proxmox - dashboard</title>
|
||||
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="modules/w3.css">
|
||||
<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/user.js" type="module"></script>
|
||||
<script src="scripts/draggable.js" type="module"></script>
|
||||
<script src="modules/Sortable.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
|
||||
<style>
|
||||
div.two-columns {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
div.two-columns * {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
draggable-container {
|
||||
height: 100%
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>proxmox</h1>
|
||||
<label for="navtoggle">☰</label>
|
||||
<input type="checkbox" id="navtoggle">
|
||||
<nav id="navigation">
|
||||
<a href="index.html">Instances</a>
|
||||
<a href="account.html">Account</a>
|
||||
<a href="settings.html">Settings</a>
|
||||
<a id="admin-link" href="admin.html" aria-current="page">Admin</a>
|
||||
<a href="login.html">Logout</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
<h2 id="name"><a href="admin.html">Admin</a> / Users / %{username}</h2>
|
||||
<form>
|
||||
<fieldset class="w3-card w3-padding">
|
||||
<legend>Groups</legend>
|
||||
<div class="input-grid two-columns">
|
||||
<p>Member Of</p>
|
||||
<p>Not Member Of</p>
|
||||
<draggable-container id="groups-enabled"></draggable-container>
|
||||
<draggable-container id="groups-disabled"></draggable-container>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="w3-card w3-padding">
|
||||
<legend>Resources</legend>
|
||||
<div id="resources" class="input-grid" style="grid-template-columns: auto auto auto auto 1fr;"></div>
|
||||
<div class="w3-container w3-center">
|
||||
<button type="button" id="resource-add" class="w3-button" aria-label="Add New Resource Constraint">
|
||||
<span class="large" style="margin: 0;">Add Resource</span>
|
||||
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Resource Constraint"><use href="images/actions/user/add.svg#symb"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="w3-card w3-padding">
|
||||
<legend>Cluster</legend>
|
||||
<div class="input-grid" style="grid-template-columns: auto auto 1fr;">
|
||||
<label for="admin">Admin</label>
|
||||
<input class="w3-check w3-border" type="checkbox" id="admin" name="admin">
|
||||
<div></div>
|
||||
<label style="align-self: start;">Nodes</label>
|
||||
<div class="grid two-columns" style="grid-column: 2 / span 2">
|
||||
<p>Allowed</p>
|
||||
<p>Not Allowed</p>
|
||||
<draggable-container id="nodes-enabled"></draggable-container>
|
||||
<draggable-container id="nodes-disabled"></draggable-container>
|
||||
</div>
|
||||
<label style="align-self: start;">Pools</label>
|
||||
<div class="grid two-columns" style="grid-column: 2 / span 2">
|
||||
<p>Allowed</p>
|
||||
<p>Not Allowed</p>
|
||||
<draggable-container id="pools-enabled"></draggable-container>
|
||||
<draggable-container id="pools-disabled"></draggable-container>
|
||||
</div>
|
||||
<label for="vmid-min">VMID Min</label>
|
||||
<input class="w3-input w3-border" type="number" id="vmid-min" name="vmid-min">
|
||||
<div></div>
|
||||
<label for="vmid-max">VMID Max</label>
|
||||
<input class="w3-input w3-border" type="number" id="vmid-max" name="vmid-max">
|
||||
<div></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="w3-container w3-center" id="form-actions">
|
||||
<button class="w3-button w3-margin" id="exit" type="button">EXIT</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user