Compare commits

..

No commits in common. "main" and "wfa-fuzzy-search" have entirely different histories.

71 changed files with 1918 additions and 2901 deletions

View File

@ -1,14 +1,14 @@
{ {
"env": { "env": {
"browser": true, "browser": true,
"es2021": true "es2021": true
}, },
"extends": "standard", "extends": "standard",
"parserOptions": { "parserOptions": {
"ecmaVersion": "latest", "ecmaVersion": "latest",
"sourceType": "module" "sourceType": "module"
}, },
"rules": { "rules": {
"no-tabs": [ "no-tabs": [
"error", "error",
{ {
@ -38,5 +38,5 @@
"allowSingleLine": false "allowSingleLine": false
} }
] ]
} }
} }

View File

@ -5,13 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title> <title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml"> <link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css"> <link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/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/style.css">
<link rel="stylesheet" href="css/nav.css"> <link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css"> <link rel="stylesheet" href="css/form.css">
<script src="scripts/account.js" type="module"></script> <script src="scripts/account.js" type="module"></script>
<script src="modules/chart.js"></script> <script src="scripts/chart.js" type="module"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style> <style>
@media screen and (width >= 1264px){ @media screen and (width >= 1264px){
@ -46,32 +46,33 @@
<h1>proxmox</h1> <h1>proxmox</h1>
<label for="navtoggle">&#9776;</label> <label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle"> <input type="checkbox" id="navtoggle">
<nav id="navigation"> <nav>
<a href="index.html">Instances</a> <a href="index.html">Instances</a>
<a href="account.html" aria-current="page">Account</a> <a href="account.html" aria-current="page">Account</a>
<a href="settings.html">Settings</a> <a href="settings.html">Settings</a>
<a id="admin-link" aria-disabled="true" class="none">Admin</a>
<a href="login.html">Logout</a> <a href="login.html">Logout</a>
</nav> </nav>
</header> </header>
<main> <main>
<h2>Account</h2> <section>
<section class="w3-card w3-padding"> <h2>Account</h2>
<h3>Account Details</h3> <div class="w3-card w3-padding">
<p id="username">Username:</p> <h3>Account Details</h3>
<p id="pool">Pools:</p> <p id="username">Username:</p>
<p id="vmid">VMID Range:</p> <p id="pool">Pools:</p>
<p id="nodes">Nodes:</p> <p id="vmid">VMID Range:</p>
</section> <p id="nodes">Nodes:</p>
<section class="w3-card w3-padding"> </div>
<div class="flex row nowrap"> <div class="w3-card w3-padding">
<h3>Password</h3> <div class="flex row nowrap">
<button class="w3-button w3-margin" id="change-password">Change Password</button> <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>
</div> </div>
</section>
<section class="w3-card w3-padding">
<h3>Cluster Resources</h3>
<div id="resource-container"></div>
</section> </section>
</main> </main>
</body> </body>

View File

@ -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">&#9776;</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>

View File

@ -5,34 +5,23 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title> <title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml"> <link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css"> <link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/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/style.css">
<link rel="stylesheet" href="css/nav.css"> <link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css"> <link rel="stylesheet" href="css/form.css">
<script src="scripts/instance.js" type="module"></script> <script src="scripts/config.js" type="module"></script>
<script src="scripts/draggable.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>
.input-grid p, .input-grid div {
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
}
</style>
</head> </head>
<body> <body>
<header> <header>
<h1>proxmox</h1> <h1>proxmox</h1>
<label for="navtoggle">&#9776;</label> <label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle"> <input type="checkbox" id="navtoggle">
<nav id="navigation"> <nav>
<a href="index.html" aria-current="page">Instances</a> <a href="index.html" aria-current="page">Instances</a>
<a href="account.html">Account</a> <a href="account.html">Account</a>
<a href="settings.html">Settings</a> <a href="settings.html">Settings</a>
<a id="admin-link" aria-disabled="true" class="none">Admin</a>
<a href="login.html">Logout</a> <a href="login.html">Logout</a>
</nav> </nav>
</header> </header>
@ -48,34 +37,22 @@
<legend>Disks</legend> <legend>Disks</legend>
<div class="input-grid" id="disks" style="grid-template-columns: auto auto 1fr auto;"></div> <div class="input-grid" id="disks" style="grid-template-columns: auto auto 1fr auto;"></div>
<div class="w3-container w3-center"> <div class="w3-container w3-center">
<button type="button" id="disk-add" class="w3-button" aria-label="Add New Disk"> <img id="disk-add" src="images/actions/disk/add-disk.svg" class="clickable" alt="Add New Disk" title="Add New Disk">
<span class="large" style="margin: 0;">Add Disk</span> <img id="cd-add" src="images/actions/disk/add-cd.svg" class="clickable none" alt="Add New CDROM" title="Add New CDROM">
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Disk"><use href="images/actions/disk/add-disk.svg#symb"></use></svg>
</button>
<button type="button" id="cd-add" class="w3-button none" aria-label="Add New CD">
<span class="large" style="margin: 0;">Add CD</span>
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New CDROM"><use href="images/actions/disk/add-cd.svg#symb"></use></svg>
</button>
</div> </div>
</fieldset> </fieldset>
<fieldset class="w3-card w3-padding"> <fieldset class="w3-card w3-padding">
<legend>Network Interfaces</legend> <legend>Network Interfaces</legend>
<div class="input-grid" id="networks" style="grid-template-columns: auto auto 1fr auto;"></div> <div class="input-grid" id="networks" style="grid-template-columns: auto auto 1fr auto;"></div>
<div class="w3-container w3-center"> <div class="w3-container w3-center">
<button type="button" id="network-add" class="w3-button" aria-label="Add New Network Interface"> <img id="network-add" src="images/actions/network/add.svg" class="clickable" alt="Add New Network Interface" title="Add New Network Interface">
<span class="large" style="margin: 0;">Add Network</span>
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Network Interface"><use href="images/actions/network/add.svg#symb"></use></svg>
</button>
</div> </div>
</fieldset> </fieldset>
<fieldset class="w3-card w3-padding none" id="devices-card"> <fieldset class="w3-card w3-padding none" id="devices-card">
<legend>PCIe Devices</legend> <legend>PCIe Devices</legend>
<div class="input-grid" id="devices" style="grid-template-columns: auto 1fr auto;"></div> <div class="input-grid" id="devices" style="grid-template-columns: auto 1fr auto;"></div>
<div class="w3-container w3-center"> <div class="w3-container w3-center">
<button type="button" id="device-add" class="w3-button" aria-label="Add New PCIe Device"> <img id="device-add" src="images/actions/device/add.svg" class="clickable" alt="Add New PCIe Device" title="Add New PCIe Device">
<span class="large" style="margin: 0;">Add Device</span>
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New PCIe Device"><use href="images/actions/device/add.svg#symb"></use></svg>
</button>
</div> </div>
</fieldset> </fieldset>
<fieldset class="w3-card w3-padding none" id="boot-card"> <fieldset class="w3-card w3-padding none" id="boot-card">

View File

@ -8,16 +8,6 @@
.input-grid * { .input-grid * {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
padding-top: 8px;
padding-bottom: 8px;
}
.input-grid input {
padding: 8px;
}
.input-grid svg {
padding: 0;
} }
.input-grid .last-item { .input-grid .last-item {
@ -60,17 +50,6 @@ input[type="radio"] {
position: inherit; position: inherit;
} }
.w3-select, select { div[draggable="true"] {
padding: 8px; cursor: grab;
}
.w3-check {
top: 0;
}
/* sibling of input-grid that is not inside another input grid */
:not(.input-grid) .input-grid + * {
display: inline-block;
width: 100%;
margin-top: 5px;
} }

View File

@ -1,30 +1,15 @@
:root { :root {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
--nav-header-text-color: black;
--nav-link-active-text-color: white;
--nav-link-active-bg-color: var(--main-bg-color, #404040);
--nav-transition-speed: 250ms; --nav-transition-speed: 250ms;
} }
@media screen and (prefers-color-scheme: dark) {
:root, :root.dark-theme {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
--nav-header-text-color: black;
--nav-link-active-text-color: white;
--nav-link-active-bg-color: var(--main-bg-color, #404040);
}
:root.light-theme {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
--nav-header-text-color: black;
--nav-link-active-text-color: black;
--nav-link-active-bg-color: var(--main-bg-color, white);
}
}
@media screen and (prefers-color-scheme: light) { @media screen and (prefers-color-scheme: light) {
:root, :root.light-theme { :root {
--nav-bg-color: black; --nav-bg-color: black;
--nav-text-color: white; --nav-text-color: white;
--nav-header-bg-color: #0f0; --nav-header-bg-color: #0f0;
@ -32,16 +17,6 @@
--nav-link-active-text-color: black; --nav-link-active-text-color: black;
--nav-link-active-bg-color: var(--main-bg-color, white); --nav-link-active-bg-color: var(--main-bg-color, white);
} }
:root.dark-theme {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
--nav-header-text-color: black;
--nav-link-active-text-color: white;
--nav-link-active-bg-color: var(--main-bg-color, #404040);
}
} }
header { header {

View File

@ -3,30 +3,16 @@
--positive-color: #0f0; --positive-color: #0f0;
--highlight-color: yellow; --highlight-color: yellow;
--lightbg-text-color: black; --lightbg-text-color: black;
} --main-bg-color: #404040;
--main-text-color: white;
@media screen and (prefers-color-scheme: dark) { --main-card-bg-color: #202020;
:root, :root.dark-theme { --main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 80%), 0 2px 10px 0 rgb(0 0 0 / 80%);
--main-bg-color: #404040; --main-table-header-bg-color: black;
--main-text-color: white; --main-input-bg-color: #404040;
--main-card-bg-color: #202020;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 80%), 0 2px 10px 0 rgb(0 0 0 / 80%);
--main-table-header-bg-color: black;
--main-input-bg-color: #404040;
}
:root.light-theme {
--main-bg-color: white;
--main-text-color: black;
--main-card-bg-color: white;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 20%), 0 2px 10px 0 rgb(0 0 0 / 20%);
--main-table-header-bg-color: #808080;
--main-input-bg-color: white;
}
} }
@media screen and (prefers-color-scheme: light) { @media screen and (prefers-color-scheme: light) {
:root, :root.light-theme { :root {
--main-bg-color: white; --main-bg-color: white;
--main-text-color: black; --main-text-color: black;
--main-card-bg-color: white; --main-card-bg-color: white;
@ -34,23 +20,13 @@
--main-table-header-bg-color: #808080; --main-table-header-bg-color: #808080;
--main-input-bg-color: white; --main-input-bg-color: white;
} }
:root.dark-theme {
--main-bg-color: #404040;
--main-text-color: white;
--main-card-bg-color: #202020;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 80%), 0 2px 10px 0 rgb(0 0 0 / 80%);
--main-table-header-bg-color: black;
--main-input-bg-color: #404040;
}
} }
html { html {
box-sizing: border-box; box-sizing: border-box;
background-color: var(--main-bg-color);
} }
* { h1, h2, h3, h4, h5, h6, p, a, label, button, input, select, td {
font-family: monospace; font-family: monospace;
} }
@ -93,14 +69,13 @@ input, select, textarea {
color: var(--main-text-color); color: var(--main-text-color);
} }
img.clickable, svg.clickable { img.clickable {
cursor: pointer; cursor: pointer;
} }
img, svg { img {
height: 1em; height: 1em;
width: 1em; width: 1em;
color: var(--main-text-color)
} }
hr, * { hr, * {
@ -138,39 +113,14 @@ hr, * {
min-height: 1em; min-height: 1em;
} }
@media screen and (width >= 440px) { .w3-select, select {
button .large { padding: 8px;
display: block; -moz-appearance: none;
} -webkit-appearance: none;
appearance: none;
button .small { appearance: none;
display: 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;
@media screen and (width <= 440px) {
button .large {
display: none;
}
button .small {
display: block;
}
}
/* add hide large class similar to w3-hide-medium and w3-hide-small */
@media (width >=993px) {
.w3-hide-large {
display: none !important;
}
}
/* fix edge case in w3-hide-medium where width between 992 and 993 */
@media (width <=993px) and (width >=601px){
.w3-hide-medium{display:none!important}
}
/* fix edge case in w3-hide-small when width between 600 and 601 */
@media (width <=601px) {
.w3-hide-small{display:none!important}
} }

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="delete" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#f00" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#f00" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 310 B

After

Width:  |  Height:  |  Size: 292 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#ffbfbf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#ffbfbf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 295 B

View File

@ -1 +0,0 @@
../../common/add.svg

Before

Width:  |  Height:  |  Size: 20 B

After

Width:  |  Height:  |  Size: 296 B

View File

@ -0,0 +1 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 20 B

After

Width:  |  Height:  |  Size: 296 B

View File

@ -1 +0,0 @@
../../common/config.svg

Before

Width:  |  Height:  |  Size: 23 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,16 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034">
<style>
:root {
color: #fff
}
@media (prefers-color-scheme:light) {
:root {
color: #000
}
}
</style>
<path
d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 23 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="add cd" viewBox="2.3 2.3 19.4 19.4" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g stroke="currentColor" stroke-width="1.25"><path d="M7 12h5m0 0h5m-5 0V7m0 5v5"/><circle cx="12" cy="12" r="9"/></g></svg> <svg width="16" height="16" viewBox="2.3 2.3 19.4 19.4" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g stroke="currentColor" stroke-width="1.25"><path d="M7 12h5m0 0h5m-5 0V7m0 5v5"/><circle cx="12" cy="12" r="9"/></g></svg>

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 313 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="create disk" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M25 0H7a7 7 0 00-7 7v18a7 7 0 007 7h18a7 7 0 007-7V7a7 7 0 00-7-7zm5 25a5 5 0 01-5 5H7a5 5 0 01-5-5V7a5 5 0 015-5h18a5 5 0 015 5z"/><path d="M17 6h-2v9H6v2h9v9h2v-9h9v-2h-9V6z"/></g></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M25 0H7a7 7 0 00-7 7v18a7 7 0 007 7h18a7 7 0 007-7V7a7 7 0 00-7-7zm5 25a5 5 0 01-5 5H7a5 5 0 01-5-5V7a5 5 0 015-5h18a5 5 0 015 5z"/><path d="M17 6h-2v9H6v2h9v9h2v-9h9v-2h-9V6z"/></g></svg>

Before

Width:  |  Height:  |  Size: 412 B

After

Width:  |  Height:  |  Size: 389 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="attach disk" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><g fill="#0f0"><path d="M17 12h-2.85a6.25 6.25 0 00-6.21 5H2v2h5.93a6.22 6.22 0 006.22 5H17z" class="prefix__prefix__clr-i-solid prefix__prefix__clr-i-solid-path-1"/><path d="M28.23 17A6.25 6.25 0 0022 12h-3v12h3a6.22 6.22 0 006.22-5H34v-2z" class="prefix__prefix__clr-i-solid prefix__prefix__clr-i-solid-path-2"/><path fill="none" d="M0 0h36v36H0z"/></g></svg> <svg width="800" height="800" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><g fill="#0f0"><path d="M17 12h-2.85a6.25 6.25 0 00-6.21 5H2v2h5.93a6.22 6.22 0 006.22 5H17z" class="prefix__prefix__clr-i-solid prefix__prefix__clr-i-solid-path-1"/><path d="M28.23 17A6.25 6.25 0 0022 12h-3v12h3a6.22 6.22 0 006.22-5H34v-2z" class="prefix__prefix__clr-i-solid prefix__prefix__clr-i-solid-path-2"/><path fill="none" d="M0 0h36v36H0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 467 B

After

Width:  |  Height:  |  Size: 446 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="detach disk" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="red" d="M76.987 235.517H0v40.973h76.987c9.04 33.686 39.694 58.522 76.238 58.522h57.062V176.988h-57.062c-36.543 0-67.206 24.836-76.238 58.529zm435.013 0h-76.995c-9.032-33.693-39.686-58.53-76.23-58.53h-57.062v158.024h57.062c36.537 0 67.19-24.836 76.23-58.522H512v-40.972z"/></svg> <svg height="800" width="800" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="red" d="M76.987 235.517H0v40.973h76.987c9.04 33.686 39.694 58.522 76.238 58.522h57.062V176.988h-57.062c-36.543 0-67.206 24.836-76.238 58.529zm435.013 0h-76.995c-9.032-33.693-39.686-58.53-76.23-58.53h-57.062v158.024h57.062c36.537 0 67.19-24.836 76.23-58.522H512v-40.972z"/></svg>

Before

Width:  |  Height:  |  Size: 398 B

After

Width:  |  Height:  |  Size: 377 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="" xmlns="http://www.w3.org/2000/svg"/> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"/>

Before

Width:  |  Height:  |  Size: 76 B

After

Width:  |  Height:  |  Size: 64 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="move disk" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M16 7l5 5m0 0l-5 5m5-5H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M16 7l5 5m0 0l-5 5m5-5H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 318 B

After

Width:  |  Height:  |  Size: 299 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 7l5 5m0 0l-5 5m5-5H3" stroke="gray" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 7l5 5m0 0l-5 5m5-5H3" stroke="gray" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 205 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="resize disk" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg> <svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 317 B

After

Width:  |  Height:  |  Size: 296 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="gray"/></svg> <svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="gray"/></svg>

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 202 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="drag" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M5 10h14m-5 9l-2 2-2-2m4-14l-2-2-2 2m-5 9h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M5 10h14m-5 9l-2 2-2-2m4-14l-2-2-2 2m-5 9h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 329 B

View File

@ -1 +0,0 @@
../../common/add.svg

View File

@ -1 +0,0 @@
../../common/config.svg

View File

@ -1 +0,0 @@
../../common/add.svg

Before

Width:  |  Height:  |  Size: 20 B

After

Width:  |  Height:  |  Size: 296 B

View File

@ -0,0 +1 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 20 B

After

Width:  |  Height:  |  Size: 296 B

View File

@ -1 +0,0 @@
../../common/config.svg

Before

Width:  |  Height:  |  Size: 23 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 23 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="#808080"/></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="#808080"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1018 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="instance console" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="currentColor"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 565 B

After

Width:  |  Height:  |  Size: 537 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="#808080"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="#808080"/></svg>

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 446 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="start instance" xmlns="http://www.w3.org/2000/svg" viewBox="2.8 2.4 12 12"><path d="M4.25 3l1.166-.624 8 5.333v1.248l-8 5.334-1.166-.624V3zm1.5 1.401v7.864l5.898-3.932L5.75 4.401z" fill="#0f0"/></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="2.8 2.4 12 12"><path d="M4.25 3l1.166-.624 8 5.333v1.248l-8 5.334-1.166-.624V3zm1.5 1.401v7.864l5.898-3.932L5.75 4.401z" fill="#0f0"/></svg>

Before

Width:  |  Height:  |  Size: 238 B

After

Width:  |  Height:  |  Size: 232 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="stop instance" xmlns="http://www.w3.org/2000/svg" viewBox="-25 -25 380 380"><path stroke-width="20" d="M315 0H15C6.716 0 0 6.716 0 15v300c0 8.284 6.716 15 15 15h300c8.284 0 15-6.716 15-15V15c0-8.284-6.716-15-15-15zm-15 300H30V30h270v270z" stroke="#f00" fill="#f00"/></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="-25 -25 380 380"><path stroke-width="20" d="M315 0H15C6.716 0 0 6.716 0 15v300c0 8.284 6.716 15 15 15h300c8.284 0 15-6.716 15-15V15c0-8.284-6.716-15-15-15zm-15 300H30V30h270v270z" stroke="#f00" fill="#f00"/></svg>

Before

Width:  |  Height:  |  Size: 310 B

After

Width:  |  Height:  |  Size: 285 B

View File

@ -1 +0,0 @@
../../common/add.svg

Before

Width:  |  Height:  |  Size: 20 B

After

Width:  |  Height:  |  Size: 296 B

View File

@ -0,0 +1 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 20 B

After

Width:  |  Height:  |  Size: 296 B

View File

@ -1 +0,0 @@
../../common/config.svg

Before

Width:  |  Height:  |  Size: 23 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 23 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +0,0 @@
../../common/add.svg

View File

@ -1 +0,0 @@
../../common/config.svg

View File

@ -1 +0,0 @@
<svg id="symb" role="img" aria-label="add device" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 316 B

View File

@ -1 +0,0 @@
<svg id="symb" role="img" aria-label="config device" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +0,0 @@
<svg role="img" aria-label="search" id="symb" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M15.796 15.811L21 21m-3-10.5a7.5 7.5 0 11-15 0 7.5 7.5 0 0115 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 367 B

View File

@ -1 +1 @@
<svg id="symb" xmlns="http://www.w3.org/2000/svg"><g fill="#0f0" font-family="monospace" font-weight="bold"><text y="14" font-size="16">H</text><text x="9" y="8" font-size="10">0</text></g></svg> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill="#0f0" font-family="monospace" font-weight="bold"><text y="14" font-size="16">H</text><text x="9" y="8" font-size="10">0</text></g></svg>

Before

Width:  |  Height:  |  Size: 195 B

After

Width:  |  Height:  |  Size: 208 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="cpu" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M14.25 8h-4.5A1.752 1.752 0 008 9.75v4.5A1.752 1.752 0 009.75 16h4.5A1.752 1.752 0 0016 14.25v-4.5A1.752 1.752 0 0014.25 8zM14 14h-4v-4h4zm8-5a1 1 0 000-2h-2v-.25A2.752 2.752 0 0017.25 4H17V2a1 1 0 00-2 0v2h-2V2a1 1 0 00-2 0v2H9V2a1 1 0 00-2 0v2h-.25A2.752 2.752 0 004 6.75V7H2a1 1 0 000 2h2v2H2a1 1 0 000 2h2v2H2a1 1 0 000 2h2v.25A2.752 2.752 0 006.75 20H7v2a1 1 0 002 0v-2h2v2a1 1 0 002 0v-2h2v2a1 1 0 002 0v-2h.25A2.752 2.752 0 0020 17.25V17h2a1 1 0 000-2h-2v-2h2a1 1 0 000-2h-2V9zm-4 8.25a.751.751 0 01-.75.75H6.75a.751.751 0 01-.75-.75V6.75A.751.751 0 016.75 6h10.5a.751.751 0 01.75.75z" fill="currentColor"/></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M14.25 8h-4.5A1.752 1.752 0 008 9.75v4.5A1.752 1.752 0 009.75 16h4.5A1.752 1.752 0 0016 14.25v-4.5A1.752 1.752 0 0014.25 8zM14 14h-4v-4h4zm8-5a1 1 0 000-2h-2v-.25A2.752 2.752 0 0017.25 4H17V2a1 1 0 00-2 0v2h-2V2a1 1 0 00-2 0v2H9V2a1 1 0 00-2 0v2h-.25A2.752 2.752 0 004 6.75V7H2a1 1 0 000 2h2v2H2a1 1 0 000 2h2v2H2a1 1 0 000 2h2v.25A2.752 2.752 0 006.75 20H7v2a1 1 0 002 0v-2h2v2a1 1 0 002 0v-2h2v2a1 1 0 002 0v-2h.25A2.752 2.752 0 0020 17.25V17h2a1 1 0 000-2h-2v-2h2a1 1 0 000-2h-2V9zm-4 8.25a.751.751 0 01-.75.75H6.75a.751.751 0 01-.75-.75V6.75A.751.751 0 016.75 6h10.5a.751.751 0 01.75.75z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 798 B

View File

@ -1 +1 @@
<svg role="img" aria-label="pci device" id="symb" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g stroke="currentColor" fill="currentColor"><path d="M480.003 128H48c0-22.056-17.944-40-40-40a8 8 0 000 16c13.234 0 24 10.766 24 24v288a8 8 0 0016 0v-8h16.01C77.238 408 88 397.238 88 384.01V384h392.003C497.646 384 512 369.646 512 352.003V159.997C512 142.354 497.646 128 480.003 128zM496 352.003c0 8.821-7.176 15.997-15.997 15.997H80a8 8 0 00-8 8v8.01c0 4.406-3.584 7.99-7.99 7.99H48V144h432.003c8.821 0 15.997 7.176 15.997 15.997v192.006z"/><path d="M240 192c-22.922 0-43.057 12.12-54.363 30.28a8.026 8.026 0 00-1.737 2.954A63.601 63.601 0 00176 256a63.583 63.583 0 008.264 31.399c.187.398.407.778.656 1.14C196.078 307.354 216.586 320 240 320c35.29 0 64-28.71 64-64a63.583 63.583 0 00-8.264-31.399 8.057 8.057 0 00-.656-1.14C283.922 204.646 263.414 192 240 192zm-48 64c0-4.395.605-8.648 1.717-12.695 3.596 3.178 8.453 6.73 15.035 10.53 6.376 3.681 11.742 6.078 16.208 7.612-2.622 2.061-5.987 4.385-10.208 6.821-8.449 4.878-14.816 7.039-18.36 7.752A47.681 47.681 0 01192 256zm96 0c0 4.103-.52 8.087-1.493 11.891-3.617-3.227-8.542-6.848-15.259-10.726-5.96-3.441-11.036-5.758-15.321-7.298 2.483-1.885 5.564-3.966 9.321-6.135 8.447-4.876 14.816-7.039 18.36-7.752A47.681 47.681 0 01288 256zm-14.052-33.901c-4.562 1.524-10.087 3.96-16.699 7.777-6.252 3.61-10.952 6.997-14.49 10.051-.449-3.245-.759-7.21-.759-11.927 0-9.763 1.314-16.361 2.469-19.785 11.465 1.064 21.775 6.169 29.479 13.884zm-46.329-12.472C226.655 214.344 226 220.354 226 228c0 7.056.557 12.721 1.401 17.26-3.022-1.232-6.59-2.938-10.65-5.282-8.302-4.793-13.33-9.159-15.769-11.883 6.394-8.915 15.757-15.56 26.637-18.468zm-21.57 80.271c4.564-1.524 10.086-3.954 16.702-7.774 6.252-3.61 10.952-6.997 14.49-10.051.449 3.245.759 7.21.759 11.927 0 9.763-1.314 16.361-2.469 19.785-11.466-1.064-21.778-6.17-29.482-13.887zm46.332 12.475C253.345 297.656 254 291.646 254 284c0-7.633-.653-13.635-1.614-18.347 3.066 1.237 6.708 2.97 10.863 5.368 8.764 5.06 13.892 9.652 16.163 12.33-6.4 9.195-15.926 16.054-27.031 19.022z"/><path d="M440 168a8 8 0 000 16c8.822 0 16 7.178 16 16v112c0 8.822-7.178 16-16 16H240c-39.701 0-72-32.299-72-72s32.299-72 72-72h168a8 8 0 000-16H240c-48.523 0-88 39.477-88 88s39.477 88 88 88h200c17.645 0 32-14.355 32-32V200c0-17.645-14.355-32-32-32zm-328 64H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88zm24-88H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88zm24 80H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88z"/><path d="M432 200h-24a8 8 0 000 16h24a8 8 0 000-16zm0 96h-24a8 8 0 000 16h24a8 8 0 000-16zm0-72h-24a8 8 0 000 16h24a8 8 0 000-16zm0 24h-24a8 8 0 000 16h24a8 8 0 000-16zm0 24h-24a8 8 0 000 16h24a8 8 0 000-16z"/></g></svg> <svg height="800" width="800" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g stroke="currentColor" fill="currentColor"><path d="M480.003 128H48c0-22.056-17.944-40-40-40a8 8 0 000 16c13.234 0 24 10.766 24 24v288a8 8 0 0016 0v-8h16.01C77.238 408 88 397.238 88 384.01V384h392.003C497.646 384 512 369.646 512 352.003V159.997C512 142.354 497.646 128 480.003 128zM496 352.003c0 8.821-7.176 15.997-15.997 15.997H80a8 8 0 00-8 8v8.01c0 4.406-3.584 7.99-7.99 7.99H48V144h432.003c8.821 0 15.997 7.176 15.997 15.997v192.006z"/><path d="M240 192c-22.922 0-43.057 12.12-54.363 30.28a8.026 8.026 0 00-1.737 2.954A63.601 63.601 0 00176 256a63.583 63.583 0 008.264 31.399c.187.398.407.778.656 1.14C196.078 307.354 216.586 320 240 320c35.29 0 64-28.71 64-64a63.583 63.583 0 00-8.264-31.399 8.057 8.057 0 00-.656-1.14C283.922 204.646 263.414 192 240 192zm-48 64c0-4.395.605-8.648 1.717-12.695 3.596 3.178 8.453 6.73 15.035 10.53 6.376 3.681 11.742 6.078 16.208 7.612-2.622 2.061-5.987 4.385-10.208 6.821-8.449 4.878-14.816 7.039-18.36 7.752A47.681 47.681 0 01192 256zm96 0c0 4.103-.52 8.087-1.493 11.891-3.617-3.227-8.542-6.848-15.259-10.726-5.96-3.441-11.036-5.758-15.321-7.298 2.483-1.885 5.564-3.966 9.321-6.135 8.447-4.876 14.816-7.039 18.36-7.752A47.681 47.681 0 01288 256zm-14.052-33.901c-4.562 1.524-10.087 3.96-16.699 7.777-6.252 3.61-10.952 6.997-14.49 10.051-.449-3.245-.759-7.21-.759-11.927 0-9.763 1.314-16.361 2.469-19.785 11.465 1.064 21.775 6.169 29.479 13.884zm-46.329-12.472C226.655 214.344 226 220.354 226 228c0 7.056.557 12.721 1.401 17.26-3.022-1.232-6.59-2.938-10.65-5.282-8.302-4.793-13.33-9.159-15.769-11.883 6.394-8.915 15.757-15.56 26.637-18.468zm-21.57 80.271c4.564-1.524 10.086-3.954 16.702-7.774 6.252-3.61 10.952-6.997 14.49-10.051.449 3.245.759 7.21.759 11.927 0 9.763-1.314 16.361-2.469 19.785-11.466-1.064-21.778-6.17-29.482-13.887zm46.332 12.475C253.345 297.656 254 291.646 254 284c0-7.633-.653-13.635-1.614-18.347 3.066 1.237 6.708 2.97 10.863 5.368 8.764 5.06 13.892 9.652 16.163 12.33-6.4 9.195-15.926 16.054-27.031 19.022z"/><path d="M440 168a8 8 0 000 16c8.822 0 16 7.178 16 16v112c0 8.822-7.178 16-16 16H240c-39.701 0-72-32.299-72-72s32.299-72 72-72h168a8 8 0 000-16H240c-48.523 0-88 39.477-88 88s39.477 88 88 88h200c17.645 0 32-14.355 32-32V200c0-17.645-14.355-32-32-32zm-328 64H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88zm24-88H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88zm24 80H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88z"/><path d="M432 200h-24a8 8 0 000 16h24a8 8 0 000-16zm0 96h-24a8 8 0 000 16h24a8 8 0 000-16zm0-72h-24a8 8 0 000 16h24a8 8 0 000-16zm0 24h-24a8 8 0 000 16h24a8 8 0 000-16zm0 24h-24a8 8 0 000 16h24a8 8 0 000-16z"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="cd" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M12 0a12 12 0 1012 12A12 12 0 0012 0zm0 22a10 10 0 1110-10 10 10 0 01-10 10z"/><path d="M12 8a4 4 0 104 4 4 4 0 00-4-4zm0 6a2 2 0 111.73-3A2 2 0 0112 14z"/><path d="M12 18a6 6 0 01-6-6 1 1 0 00-2 0 8 8 0 008 8 1 1 0 000-2zm0-14a1 1 0 000 2 6 6 0 016 6 1 1 0 001 1c2.57 0 .27-9-7-9z"/></g></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M12 0a12 12 0 1012 12A12 12 0 0012 0zm0 22a10 10 0 1110-10 10 10 0 01-10 10z"/><path d="M12 8a4 4 0 104 4 4 4 0 00-4-4zm0 6a2 2 0 111.73-3A2 2 0 0112 14z"/><path d="M12 18a6 6 0 01-6-6 1 1 0 00-2 0 8 8 0 008 8 1 1 0 000-2zm0-14a1 1 0 000 2 6 6 0 016 6 1 1 0 001 1c2.57 0 .27-9-7-9z"/></g></svg>

Before

Width:  |  Height:  |  Size: 509 B

After

Width:  |  Height:  |  Size: 495 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="disk" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M22 12H2m3.45-6.89L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11zM6 16h0m4 0h0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M22 12H2m3.45-6.89L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11zM6 16h0m4 0h0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 405 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="network" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M9 7V5H7v2H5v4h6V7H9zm-9 9h16V0H0v16zm2-2V2h12v12H2z" fill="currentColor" fill-rule="evenodd"/></svg> <svg width="800" height="800" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M9 7V5H7v2H5v4h6V7H9zm-9 9h16V0H0v16zm2-2V2h12v12H2z" fill="currentColor" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 298 B

After

Width:  |  Height:  |  Size: 281 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="memory" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M23 9a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v3a1 1 0 001 1 1 1 0 010 2 1 1 0 00-1 1v7a1 1 0 001 1h13a1 1 0 001-1v-3h2v3a1 1 0 001 1h5a1 1 0 001-1v-7a1 1 0 00-1-1 1 1 0 010-2zM2 16h3v2H2zm7 0v2H7v-2zm4 2h-2v-2h2zm6 0v-2h3v2zm3-10.83a3 3 0 000 5.66V14H2v-1.17a3 3 0 000-5.66V6h20z"/><path d="M9 11V9a1 1 0 00-2 0v2a1 1 0 002 0zm4 0V9a1 1 0 00-2 0v2a1 1 0 002 0zm4 0V9a1 1 0 00-2 0v2a1 1 0 002 0z"/></g></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M23 9a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v3a1 1 0 001 1 1 1 0 010 2 1 1 0 00-1 1v7a1 1 0 001 1h13a1 1 0 001-1v-3h2v3a1 1 0 001 1h5a1 1 0 001-1v-7a1 1 0 00-1-1 1 1 0 010-2zM2 16h3v2H2zm7 0v2H7v-2zm4 2h-2v-2h2zm6 0v-2h3v2zm3-10.83a3 3 0 000 5.66V14H2v-1.17a3 3 0 000-5.66V6h20z"/><path d="M9 11V9a1 1 0 00-2 0v2a1 1 0 002 0zm4 0V9a1 1 0 00-2 0v2a1 1 0 002 0zm4 0V9a1 1 0 00-2 0v2a1 1 0 002 0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 610 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="swap" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M19.841 3.24A10.988 10.988 0 008.54.573l1.266 3.8a7.033 7.033 0 018.809 9.158L17 11.891v7.092h7l-2.407-2.439A11.049 11.049 0 0019.841 3.24zM1 10.942a11.05 11.05 0 0011.013 11.044 11.114 11.114 0 003.521-.575l-1.266-3.8a7.035 7.035 0 01-8.788-9.22L7 9.891V6.034c.021-.02.038-.044.06-.065L7 5.909V2.982H0l2.482 2.449A10.951 10.951 0 001 10.942z" fill="currentColor"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="21.986"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M19.841 3.24A10.988 10.988 0 008.54.573l1.266 3.8a7.033 7.033 0 018.809 9.158L17 11.891v7.092h7l-2.407-2.439A11.049 11.049 0 0019.841 3.24zM1 10.942a11.05 11.05 0 0011.013 11.044 11.114 11.114 0 003.521-.575l-1.266-3.8a7.035 7.035 0 01-8.788-9.22L7 9.891V6.034c.021-.02.038-.044.06-.065L7 5.909V2.982H0l2.482 2.449A10.951 10.951 0 001 10.942z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 565 B

After

Width:  |  Height:  |  Size: 533 B

1
images/static/search.svg Normal file
View File

@ -0,0 +1 @@
<svg width="800" height="800" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M15.796 15.811L21 21m-3-10.5a7.5 7.5 0 11-15 0 7.5 7.5 0 0115 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="active" fill="#0f0" viewBox="0 0 16 16" stroke="#none" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="8"/></svg> <svg width="16" height="16" fill="#0f0" stroke="#none" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="8"/></svg>

Before

Width:  |  Height:  |  Size: 163 B

After

Width:  |  Height:  |  Size: 125 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="inactive" fill="#f00" viewBox="0 0 16 16" stroke="none" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="8"/></svg> <svg width="16" height="16" fill="#f00" stroke="none" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="8"/></svg>

Before

Width:  |  Height:  |  Size: 164 B

After

Width:  |  Height:  |  Size: 124 B

View File

@ -1 +1 @@
<svg id="symb" role="img" aria-label="loading" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 204.481 204.481"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M162.116 38.31a7.43 7.43 0 00.454-.67c.033-.055.068-.109.1-.164a7.72 7.72 0 00.419-.857c.014-.034.024-.069.038-.104a7.492 7.492 0 00.314-1.008c.068-.288.124-.581.157-.881l.008-.052a7.48 7.48 0 00.043-.796V7.5a7.5 7.5 0 00-7.5-7.5H48.332a7.5 7.5 0 00-7.5 7.5v26.279c0 .269.016.534.043.796l.008.052c.034.3.089.593.157.881.016.069.035.138.053.207.073.273.159.541.261.801.013.034.024.069.038.104.121.296.262.581.419.857.032.056.067.109.1.164.14.232.291.455.454.67.027.035.047.074.074.109l50.255 63.821-50.255 63.821c-.028.035-.047.074-.074.109a7.43 7.43 0 00-.454.67c-.033.055-.068.109-.1.164a7.72 7.72 0 00-.419.857c-.014.034-.024.069-.038.104a7.492 7.492 0 00-.314 1.008 7.308 7.308 0 00-.157.881l-.008.052a7.48 7.48 0 00-.043.796v26.279a7.5 7.5 0 007.5 7.5h107.817a7.5 7.5 0 007.5-7.5v-26.279c0-.269-.016-.534-.043-.796l-.008-.052a7.51 7.51 0 00-.157-.881c-.016-.069-.035-.138-.053-.207a7.492 7.492 0 00-.261-.801c-.013-.034-.024-.069-.038-.104a7.383 7.383 0 00-.419-.857c-.032-.056-.067-.109-.1-.164a7.646 7.646 0 00-.454-.67c-.027-.035-.047-.074-.074-.109l-50.255-63.821 50.255-63.821c.028-.035.047-.074.074-.11zM148.649 15v11.279H55.832V15h92.817zM55.832 189.481v-11.279h92.817v11.279H55.832zm84.866-26.279H63.784l38.457-48.838 38.457 48.838zm-38.457-73.084L63.784 41.279h76.914l-38.457 48.839z" fill="currentColor"/></svg> <svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 204.481 204.481"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M162.116 38.31a7.43 7.43 0 00.454-.67c.033-.055.068-.109.1-.164a7.72 7.72 0 00.419-.857c.014-.034.024-.069.038-.104a7.492 7.492 0 00.314-1.008c.068-.288.124-.581.157-.881l.008-.052a7.48 7.48 0 00.043-.796V7.5a7.5 7.5 0 00-7.5-7.5H48.332a7.5 7.5 0 00-7.5 7.5v26.279c0 .269.016.534.043.796l.008.052c.034.3.089.593.157.881.016.069.035.138.053.207.073.273.159.541.261.801.013.034.024.069.038.104.121.296.262.581.419.857.032.056.067.109.1.164.14.232.291.455.454.67.027.035.047.074.074.109l50.255 63.821-50.255 63.821c-.028.035-.047.074-.074.109a7.43 7.43 0 00-.454.67c-.033.055-.068.109-.1.164a7.72 7.72 0 00-.419.857c-.014.034-.024.069-.038.104a7.492 7.492 0 00-.314 1.008 7.308 7.308 0 00-.157.881l-.008.052a7.48 7.48 0 00-.043.796v26.279a7.5 7.5 0 007.5 7.5h107.817a7.5 7.5 0 007.5-7.5v-26.279c0-.269-.016-.534-.043-.796l-.008-.052a7.51 7.51 0 00-.157-.881c-.016-.069-.035-.138-.053-.207a7.492 7.492 0 00-.261-.801c-.013-.034-.024-.069-.038-.104a7.383 7.383 0 00-.419-.857c-.032-.056-.067-.109-.1-.164a7.646 7.646 0 00-.454-.67c-.027-.035-.047-.074-.074-.109l-50.255-63.821 50.255-63.821c.028-.035.047-.074.074-.11zM148.649 15v11.279H55.832V15h92.817zM55.832 189.481v-11.279h92.817v11.279H55.832zm84.866-26.279H63.784l38.457-48.838 38.457 48.838zm-38.457-73.084L63.784 41.279h76.914l-38.457 48.839z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -5,12 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title> <title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml"> <link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css"> <link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/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/style.css">
<link rel="stylesheet" href="css/nav.css"> <link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css"> <link rel="stylesheet" href="css/form.css">
<script src="scripts/index.js" type="module"></script> <script src="scripts/index.js" type="module"></script>
<script src="scripts/instance.js" type="module"></script>
<script src="modules/wfa.js" type="module"></script> <script src="modules/wfa.js" type="module"></script>
<style> <style>
#instance-container > div { #instance-container > div {
@ -23,11 +24,23 @@
#vm-search { #vm-search {
max-width: calc(100% - 10px - 152px); max-width: calc(100% - 10px - 152px);
} }
button .large {
display: block;
}
button .small {
display: none;
}
} }
@media screen and (width <= 440px) { @media screen and (width <= 440px) {
#vm-search { #vm-search {
max-width: calc(100% - 10px - 47px); max-width: calc(100% - 10px - 47px);
} }
button .large {
display: none;
}
button .small {
display: block;
}
} }
</style> </style>
</head> </head>
@ -36,11 +49,10 @@
<h1>proxmox</h1> <h1>proxmox</h1>
<label for="navtoggle">&#9776;</label> <label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle"> <input type="checkbox" id="navtoggle">
<nav id="navigation"> <nav>
<a href="index.html" aria-current="page">Instances</a> <a href="index.html" aria-current="page">Instances</a>
<a href="account.html">Account</a> <a href="account.html">Account</a>
<a href="settings.html">Settings</a> <a href="settings.html">Settings</a>
<a id="admin-link" aria-disabled="true" class="none">Admin</a>
<a href="login.html">Logout</a> <a href="login.html">Logout</a>
</nav> </nav>
</header> </header>
@ -50,23 +62,37 @@
<div class="w3-card w3-padding"> <div class="w3-card w3-padding">
<div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;"> <div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;">
<form id="vm-search" role="search" class="flex row nowrap"> <form id="vm-search" role="search" class="flex row nowrap">
<svg role="img" aria-label="Search Instances"><use href="images/common/search.svg#symb"></use></svg> <img src="images/static/search.svg" alt="Search VMs">
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name"> <input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
</form> </form>
<button type="button" id="instance-add" class="w3-button" aria-label="create new instance"> <button type="button" id="instance-add" class="w3-button" aria-label="create new instance">
<span class="large" style="margin: 0;">Create Instance</span> <span class="large" style="margin: 0;">Create Instance</span>
<svg class="small" style="height: 1lh; width: 1lh;" role="img" aria-label="Create New Instance"><use href="images/actions/instance/add.svg#symb"></use></svg> <img class="small" style="height: 1lh; width: 1lh;" src="images/actions/instance/add.svg" alt="Create New Instance">
</button> </button>
</div> </div>
<div> <div>
<div class="w3-row w3-hide-small" style="border-bottom: 1px solid;"> <div class="w3-row w3-hide-small" style="border-bottom: 1px solid;">
<p class="w3-col l1 m2 w3-hide-small">ID</p> <div class="w3-col l1 m2">
<p class="w3-col l2 m3 w3-hide-small">Name</p> <p>ID</p>
<p class="w3-col l1 m2 w3-hide-small">Type</p> </div>
<p class="w3-col l2 m3 w3-hide-small">Status</p> <div class="w3-col l2 m3">
<p class="w3-col l2 w3-hide-medium w3-hide-small">Host Name</p> <p>Name</p>
<p class="w3-col l2 w3-hide-medium w3-hide-small">Host Status</p> </div>
<p class="w3-col l2 m2 w3-hide-small">Actions</p> <div class="w3-col l1 m2">
<p>Type</p>
</div>
<div class="w3-col l2 m3">
<p>Status</p>
</div>
<div class="w3-col l2 w3-hide-medium">
<p>Host Name</p>
</div>
<div class="w3-col l2 w3-hide-medium">
<p>Host Status</p>
</div>
<div class="w3-col l2 m2">
<p>Actions</p>
</div>
</div> </div>
<div id="instance-container"></div> <div id="instance-container"></div>
</div> </div>

View File

@ -5,11 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title> <title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml"> <link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css"> <link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/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/style.css">
<link rel="stylesheet" href="css/nav.css"> <link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/login.js" type="module"></script> <script src="scripts/login.js" type="module"></script>
</head> </head>
<body> <body>
@ -17,7 +16,7 @@
<h1>proxmox</h1> <h1>proxmox</h1>
<label for="navtoggle">&#9776;</label> <label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle"> <input type="checkbox" id="navtoggle">
<nav id="navigation"> <nav>
<a href="login.html" aria-current="page">Login</a> <a href="login.html" aria-current="page">Login</a>
</nav> </nav>
</header> </header>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -4,8 +4,7 @@
"description": "Front-end for ProxmoxAAS", "description": "Front-end for ProxmoxAAS",
"type": "module", "type": "module",
"scripts": { "scripts": {
"lint": "html-validator --continue; stylelint --formatter verbose --fix css/*.css; DEBUG=eslint:cli-engine eslint --fix scripts/", "lint": "html-validator --continue; stylelint --formatter verbose --fix **/*.css; DEBUG=eslint:cli-engine eslint --fix scripts/"
"update-modules": "rm -rf modules/wfa.js modules/wfa.wasm; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.js -o modules/wfa.js; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.wasm -o modules/wfa.wasm"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.43.0", "eslint": "^8.43.0",

View File

@ -1,122 +1,5 @@
import { dialog } from "./dialog.js"; import { dialog } from "./dialog.js";
import { requestAPI, goToPage, getCookie, setTitleAndHeader, setAppearance } from "./utils.js"; import { requestAPI, goToPage, getCookie, setTitleAndHeader } from "./utils.js";
class ResourceChart extends HTMLElement {
constructor () {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
* {
box-sizing: border-box;
font-family: monospace;
}
figure {
margin: 0;
}
div {
max-width: 400px;
aspect-ratio: 1 / 1;
}
figcaption {
text-align: center;
margin-top: 10px;
display: flex;
flex-direction: column;
}
</style>
<style id="responsive-style" media="not all">
figure {
display: flex;
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
}
div {
max-height: 1lh;
}
figcaption {
margin: 0;
margin-left: 10px;
display: flex;
flex-direction: row;
gap: 1ch;
font-size: small;
}
</style>
<figure>
<div>
<canvas></canvas>
</div>
<figcaption></figcaption>
</figure>
`;
this.responsiveStyle = this.shadowRoot.querySelector("#responsive-style");
this.canvas = this.shadowRoot.querySelector("canvas");
this.caption = this.shadowRoot.querySelector("figcaption");
}
set data (data) {
for (const line of data.title) {
this.caption.innerHTML += `<span>${line}</span>`;
}
this.canvas.role = "img";
this.canvas.ariaLabel = data.ariaLabel;
const chartData = {
type: "pie",
data: data.data,
options: {
plugins: {
title: {
display: false
},
legend: {
display: false
},
tooltip: {
enabled: true
}
},
interaction: {
mode: "nearest"
},
onHover: function (e, activeElements) {
if (window.innerWidth <= data.breakpoint) {
updateTooltipShow(e.chart, false);
}
else {
updateTooltipShow(e.chart, true);
}
}
}
};
this.chart = new window.Chart(this.canvas, chartData);
if (data.breakpoint) {
this.responsiveStyle.media = `screen and (width <= ${data.breakpoint}px)`;
}
else {
this.responsiveStyle.media = "not all";
}
}
get data () {
return null;
}
}
// this is a really bad way to do this, but chartjs api does not expose many ways to dynamically set hover and tooltip options
function updateTooltipShow (chart, enabled) {
chart.options.plugins.tooltip.enabled = enabled;
chart.options.interaction.mode = enabled ? "nearest" : null;
chart.update();
}
customElements.define("resource-chart", ResourceChart);
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
@ -138,7 +21,6 @@ const prefixes = {
}; };
async function init () { async function init () {
setAppearance();
setTitleAndHeader(); setTitleAndHeader();
const cookie = document.cookie; const cookie = document.cookie;
if (cookie === "") { if (cookie === "") {
@ -150,7 +32,7 @@ async function init () {
let userCluster = requestAPI("/user/config/cluster"); let userCluster = requestAPI("/user/config/cluster");
resources = await resources; resources = await resources;
meta = (await meta).resources; meta = await meta;
userCluster = await userCluster; userCluster = await userCluster;
document.querySelector("#username").innerText = `Username: ${getCookie("username")}`; document.querySelector("#username").innerText = `Username: ${getCookie("username")}`;
@ -248,7 +130,7 @@ function handlePasswordChangeForm () {
`; `;
const d = dialog("Change Password", body, async (result, form) => { const d = dialog("Change Password", body, async (result, form) => {
if (result === "confirm") { if (result === "confirm") {
const result = await requestAPI("/access/password", "POST", { password: form.get("new-password") }); const result = await requestAPI("/auth/password", "POST", { password: form.get("new-password") });
if (result.status !== 200) { if (result.status !== 200) {
alert(result.error); alert(result.error);
} }

View File

@ -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 = "&nbsp;";
}
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
}

120
scripts/chart.js Normal file
View File

@ -0,0 +1,120 @@
class ResourceChart extends HTMLElement {
constructor () {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
* {
box-sizing: border-box;
font-family: monospace;
}
figure {
margin: 0;
}
div {
max-width: 400px;
aspect-ratio: 1 / 1;
}
figcaption {
text-align: center;
margin-top: 10px;
display: flex;
flex-direction: column;
}
</style>
<style id="responsive-style" media="not all">
figure {
display: flex;
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
}
div {
max-height: 1lh;
}
figcaption {
margin: 0;
margin-left: 10px;
display: flex;
flex-direction: row;
gap: 1ch;
font-size: small;
}
</style>
<figure>
<div>
<canvas></canvas>
</div>
<figcaption></figcaption>
</figure>
`;
this.responsiveStyle = this.shadowRoot.querySelector("#responsive-style");
this.canvas = this.shadowRoot.querySelector("canvas");
this.caption = this.shadowRoot.querySelector("figcaption");
}
set data (data) {
for (const line of data.title) {
this.caption.innerHTML += `<span>${line}</span>`;
}
this.canvas.role = "img";
this.canvas.ariaLabel = data.ariaLabel;
const chartData = {
type: "pie",
data: data.data,
options: {
plugins: {
title: {
display: false
},
legend: {
display: false
},
tooltip: {
enabled: true
}
},
interaction: {
mode: "nearest"
},
onHover: function (e, activeElements) {
if (window.innerWidth <= data.breakpoint) {
updateTooltipShow(e.chart, false);
}
else {
updateTooltipShow(e.chart, true);
}
}
}
};
this.chart = createChart(this.canvas, chartData);
if (data.breakpoint) {
this.responsiveStyle.media = `screen and (width <= ${data.breakpoint}px)`;
}
else {
this.responsiveStyle.media = "not all";
}
}
get data () {
return null;
}
}
function createChart (ctx, data) {
return new window.Chart(ctx, data);
}
// this is a really bad way to do this, but chartjs api does not expose many ways to dynamically set hover and tooltip options
function updateTooltipShow (chart, enabled) {
chart.options.plugins.tooltip.enabled = enabled;
chart.options.interaction.mode = enabled ? "nearest" : null;
chart.update();
}
customElements.define("resource-chart", ResourceChart);

View File

@ -1,8 +1,17 @@
import { getSyncSettings, requestAPI } from "./utils.js"; import { requestAPI } from "./utils.js";
import { API } from "../vars.js"; import { API } from "../vars.js";
export async function setupClientSync (callback) { export async function setupClientSync (callback) {
const { scheme, rate } = getSyncSettings(); let scheme = localStorage.getItem("sync-scheme");
let rate = Number(localStorage.getItem("sync-rate"));
if (!scheme) {
scheme = "always";
localStorage.setItem("sync-scheme", "always");
}
if (!rate) {
rate = "5";
localStorage.setItem("sync-rate", "5");
}
if (scheme === "always") { if (scheme === "always") {
callback(); callback();

861
scripts/config.js Normal file
View File

@ -0,0 +1,861 @@
import { requestPVE, requestAPI, goToPage, getURIData, resourcesConfig, setTitleAndHeader, bootConfig } from "./utils.js";
import { alert, dialog } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init); // do the dumb thing where the disk config refreshes every second
const diskMetaData = resourcesConfig.disk;
const networkMetaData = resourcesConfig.network;
const pcieMetaData = resourcesConfig.pcie;
const bootMetaData = bootConfig;
let node;
let type;
let vmid;
let config;
async function init () {
setTitleAndHeader();
const cookie = document.cookie;
if (cookie === "") {
goToPage("login.html");
}
const uriData = getURIData();
node = uriData.node;
type = uriData.type;
vmid = uriData.vmid;
await getConfig();
populateResources();
populateDisk();
populateNetworks();
populateDevices();
populateBoot();
document.querySelector("#exit").addEventListener("click", handleFormExit);
}
function getOrdered (keys) {
const orderedKeys = Object.keys(keys).sort((a, b) => {
return parseInt(a) - parseInt(b);
}); // ordered integer list
return orderedKeys;
}
async function getConfig () {
config = await requestPVE(`/nodes/${node}/${type}/${vmid}/config`, "GET");
}
async function populateResources () {
const name = type === "qemu" ? "name" : "hostname";
document.querySelector("#name").innerHTML = document.querySelector("#name").innerHTML.replace("%{vmname}", config.data[name]);
if (type === "qemu") {
const global = await requestAPI("/global/config/resources");
const user = await requestAPI("/user/config/resources");
let options = [];
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) => {
return a.localeCompare(b);
});
}
else {
const supported = await requestPVE(`/nodes/${node}/capabilities/qemu/cpu`);
supported.data.forEach((supportedType) => {
if (!userCPU.some((userType) => supportedType.name === userType.name)) {
options.push(supportedType.name);
}
});
options = options.sort((a, b) => {
return a.localeCompare(b);
});
}
addResourceLine("resources", "images/resources/cpu.svg", "select", "CPU Type", "proctype", { value: config.data.cpu, options });
}
addResourceLine("resources", "images/resources/cpu.svg", "input", "CPU Amount", "cores", { type: "number", value: config.data.cores, min: 1, max: 8192 }, "Cores");
addResourceLine("resources", "images/resources/ram.svg", "input", "Memory", "ram", { type: "number", value: config.data.memory, min: 16, step: 1 }, "MiB");
if (type === "lxc") {
addResourceLine("resources", "images/resources/swap.svg", "input", "Swap", "swap", { type: "number", value: config.data.swap, min: 0, step: 1 }, "MiB");
}
}
function addResourceLine (fieldset, iconHref, type, labelText, id, attributes, unitText = null) {
const field = document.querySelector(`#${fieldset}`);
const icon = document.createElement("img");
icon.src = iconHref;
icon.alt = labelText;
field.append(icon);
const label = document.createElement("label");
label.innerText = labelText;
label.htmlFor = id;
field.append(label);
if (type === "input") {
const input = document.createElement("input");
for (const k in attributes) {
input.setAttribute(k, attributes[k]);
}
input.id = id;
input.name = id;
input.required = true;
input.classList.add("w3-input");
input.classList.add("w3-border");
field.append(input);
}
else if (type === "select") {
const select = document.createElement("select");
for (const option of attributes.options) {
select.append(new Option(option));
}
select.value = attributes.value;
select.id = id;
select.name = id;
select.required = true;
select.classList.add("w3-select");
select.classList.add("w3-border");
field.append(select);
}
if (unitText) {
const unit = document.createElement("p");
unit.innerText = unitText;
field.append(unit);
}
else {
const unit = document.createElement("div");
unit.classList.add("hidden");
field.append(unit);
}
}
async function populateDisk () {
document.querySelector("#disks").innerHTML = "";
for (let i = 0; i < diskMetaData[type].prefixOrder.length; i++) {
const prefix = diskMetaData[type].prefixOrder[i];
const busName = diskMetaData[type][prefix].name;
const disks = {};
Object.keys(config.data).forEach((element) => {
if (element.startsWith(prefix)) {
disks[element.replace(prefix, "")] = config.data[element];
}
});
const orderedKeys = getOrdered(disks);
orderedKeys.forEach((element) => {
const disk = disks[element];
addDiskLine("disks", prefix, busName, element, disk);
});
}
document.querySelector("#disk-add").addEventListener("click", handleDiskAdd);
if (type === "qemu") {
document.querySelector("#cd-add").classList.remove("none");
document.querySelector("#cd-add").addEventListener("click", handleCDAdd);
}
}
function addDiskLine (fieldset, busPrefix, busName, device, diskDetails) {
const field = document.querySelector(`#${fieldset}`);
const diskName = `${busName} ${device}`;
const diskID = `${busPrefix}${device}`;
// Set the disk icon, either drive.svg or disk.svg
const icon = document.createElement("img");
icon.src = diskMetaData[type][busPrefix].icon;
icon.alt = diskName;
icon.dataset.disk = diskID;
field.append(icon);
// Add a label for the disk bus and device number
const diskLabel = document.createElement("p");
diskLabel.innerText = diskName;
diskLabel.dataset.disk = diskID;
field.append(diskLabel);
// Add text of the disk configuration
const diskDesc = document.createElement("p");
diskDesc.innerText = diskDetails;
diskDesc.dataset.disk = diskID;
diskDesc.style.overflowX = "hidden";
diskDesc.style.whiteSpace = "nowrap";
field.append(diskDesc);
const actionDiv = document.createElement("div");
diskMetaData.actionBarOrder.forEach((element) => {
const action = document.createElement("img");
if (element === "detach_attach" && diskMetaData[type][busPrefix].actions.includes("attach")) { // attach
action.src = "images/actions/disk/attach.svg";
action.title = "Attach Disk";
action.addEventListener("click", handleDiskAttach);
action.classList.add("clickable");
}
else if (element === "detach_attach" && diskMetaData[type][busPrefix].actions.includes("detach")) { // detach
action.src = "images/actions/disk/detach.svg";
action.title = "Detach Disk";
action.addEventListener("click", handleDiskDetach);
action.classList.add("clickable");
}
else if (element === "delete") {
const active = diskMetaData[type][busPrefix].actions.includes(element) ? "active" : "inactive"; // resize
action.src = `images/actions/delete-${active}.svg`;
action.title = "Delete Disk";
if (active === "active") {
action.addEventListener("click", handleDiskDelete);
action.classList.add("clickable");
}
}
else {
const active = diskMetaData[type][busPrefix].actions.includes(element) ? "active" : "inactive"; // resize
action.src = `images/actions/disk/${element}-${active}.svg`;
if (active === "active") {
action.title = `${element.charAt(0).toUpperCase()}${element.slice(1)} Disk`;
if (element === "move") {
action.addEventListener("click", handleDiskMove);
}
else if (element === "resize") {
action.addEventListener("click", handleDiskResize);
}
action.classList.add("clickable");
}
}
action.dataset.disk = diskID;
action.alt = action.title;
actionDiv.append(action);
});
field.append(actionDiv);
}
async function handleDiskDetach () {
const disk = this.dataset.disk;
const header = `Detach ${disk}`;
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";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/detach`, "POST");
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
deleteBootLine(`boot-${disk}`);
}
});
}
async function handleDiskAttach () {
const header = `Attach ${this.dataset.disk}`;
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") {
const device = form.get("device");
document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg";
const body = {
source: this.dataset.disk.replace("unused", "")
};
const prefix = type === "qemu" ? "sata" : "mp";
const disk = `${prefix}${device}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/attach`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
addBootLine("disabled", { id: disk, prefix, value: disk, detail: config.data[disk] });
}
});
}
async function handleDiskResize () {
const header = `Resize ${this.dataset.disk}`;
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") {
const disk = this.dataset.disk;
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const body = {
size: form.get("size-increment")
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/resize`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
const prefix = bootMetaData.eligiblePrefixes.find((pref) => disk.startsWith(pref));
updateBootLine(`boot-${disk}`, { id: disk, prefix, value: disk, detail: config.data[disk] });
}
});
}
async function handleDiskMove () {
const content = type === "qemu" ? "images" : "rootdir";
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
const header = `Move ${this.dataset.disk}`;
let options = "";
storage.data.forEach((element) => {
if (element.content.includes(content)) {
options += `<option value="${element.storage}">${element.storage}</option>"`;
}
});
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 = `
<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) => {
if (result === "confirm") {
const disk = this.dataset.disk;
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const body = {
storage: form.get("storage-select"),
delete: form.get("delete-check") === "on" ? "1" : "0"
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/move`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
const prefix = bootMetaData.eligiblePrefixes.find((pref) => disk.startsWith(pref));
updateBootLine(`boot-${disk}`, { id: disk, prefix, value: config.data[disk] });
}
});
}
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${disk}</p>`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/delete`, "DELETE");
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
deleteBootLine(`boot-${disk}`);
}
});
}
async function handleDiskAdd () {
const content = type === "qemu" ? "images" : "rootdir";
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
const header = "Create New Disk";
let options = "";
storage.data.forEach((element) => {
if (element.content.includes(content)) {
options += `<option value="${element.storage}">${element.storage}</option>"`;
}
});
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 = `
<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) => {
if (result === "confirm") {
const body = {
storage: form.get("storage-select"),
size: form.get("size")
};
const id = form.get("device");
const prefix = type === "qemu" ? "sata" : "mp";
const disk = `${prefix}${id}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
addBootLine("disabled", { id: disk, prefix, value: disk, detail: config.data[disk] });
}
});
}
async function handleCDAdd () {
const isos = await requestAPI("/user/vm-isos", "GET");
const header = "Add a CDROM";
const body = `
<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) => {
if (result === "confirm") {
const body = {
iso: form.get("iso-select")
};
const disk = `ide${form.get("device")}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
addBootLine("disabled", { id: disk, prefix: "ide", value: disk, detail: config.data[disk] });
}
});
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 () {
document.querySelector("#networks").innerHTML = "";
const networks = {};
const prefix = networkMetaData.prefix;
Object.keys(config.data).forEach((element) => {
if (element.startsWith(prefix)) {
networks[element.replace(prefix, "")] = config.data[element];
}
});
const orderedKeys = getOrdered(networks);
orderedKeys.forEach((element) => {
addNetworkLine("networks", prefix, element, networks[element]);
});
document.querySelector("#network-add").addEventListener("click", handleNetworkAdd);
}
function addNetworkLine (fieldset, prefix, netID, netDetails) {
const field = document.querySelector(`#${fieldset}`);
const icon = document.createElement("img");
icon.src = "images/resources/network.svg";
icon.alt = `${prefix}${netID}`;
icon.dataset.network = netID;
icon.dataset.values = netDetails;
field.appendChild(icon);
const netLabel = document.createElement("p");
netLabel.innerText = `${prefix}${netID}`;
netLabel.dataset.network = netID;
netLabel.dataset.values = netDetails;
field.append(netLabel);
const netDesc = document.createElement("p");
netDesc.innerText = netDetails;
netDesc.dataset.network = netID;
netDesc.dataset.values = netDetails;
netDesc.style.overflowX = "hidden";
netDesc.style.whiteSpace = "nowrap";
field.append(netDesc);
const actionDiv = document.createElement("div");
const configBtn = document.createElement("img");
configBtn.classList.add("clickable");
configBtn.src = "images/actions/network/config.svg";
configBtn.title = "Config Interface";
configBtn.addEventListener("click", handleNetworkConfig);
configBtn.dataset.network = netID;
configBtn.dataset.values = netDetails;
actionDiv.appendChild(configBtn);
const deleteBtn = document.createElement("img");
deleteBtn.classList.add("clickable");
deleteBtn.src = "images/actions/delete-active.svg";
deleteBtn.title = "Delete Interface";
deleteBtn.addEventListener("click", handleNetworkDelete);
deleteBtn.dataset.network = netID;
deleteBtn.dataset.values = netDetails;
actionDiv.appendChild(deleteBtn);
field.append(actionDiv);
}
async function handleNetworkConfig () {
const netID = this.dataset.network;
const netDetails = this.dataset.values;
const header = `Edit net${netID}`;
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") {
document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg";
const body = {
rate: form.get("rate")
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/modify`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateNetworks();
const id = `net${netID}`;
updateBootLine(`boot-net${netID}`, { id, prefix: "net", value: id, detail: config.data[`net${netID}`] });
}
});
d.querySelector("#rate").value = netDetails.split("rate=")[1].split(",")[0];
}
async function handleNetworkDelete () {
const netID = this.dataset.network;
const header = `Delete net${netID}`;
const body = "";
dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/delete`, "DELETE");
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateNetworks();
deleteBootLine(`boot-net${netID}`);
}
});
}
async function handleNetworkAdd () {
const header = "Create Network Interface";
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") {
const body = {
rate: form.get("rate")
};
if (type === "lxc") {
body.name = form.get("name");
}
const netID = form.get("netid");
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/create`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateNetworks();
const id = `net${netID}`;
addBootLine("disabled", { id, prefix: "net", value: id, detail: config.data[`net${netID}`] });
}
});
}
async function populateDevices () {
if (type === "qemu") {
document.querySelector("#devices-card").classList.remove("none");
document.querySelector("#devices").innerHTML = "";
const devices = {};
const prefix = pcieMetaData.prefix;
Object.keys(config.data).forEach((element) => {
if (element.startsWith(prefix)) {
devices[element.replace(prefix, "")] = config.data[element];
}
});
const orderedKeys = getOrdered(devices);
orderedKeys.forEach(async (element) => {
const deviceData = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${element}`, "GET");
addDeviceLine("devices", prefix, element, devices[element], deviceData.device_name);
});
document.querySelector("#device-add").addEventListener("click", handleDeviceAdd);
}
}
function addDeviceLine (fieldset, prefix, deviceID, deviceDetails, deviceName) {
const field = document.querySelector(`#${fieldset}`);
const icon = document.createElement("img");
icon.src = "images/resources/device.svg";
icon.alt = `${prefix}${deviceID}`;
icon.dataset.device = deviceID;
icon.dataset.values = deviceDetails;
icon.dataset.name = deviceName;
field.appendChild(icon);
const deviceLabel = document.createElement("p");
deviceLabel.innerText = deviceName;
deviceLabel.dataset.device = deviceID;
deviceLabel.dataset.values = deviceDetails;
deviceLabel.dataset.name = deviceName;
deviceLabel.style.overflowX = "hidden";
deviceLabel.style.whiteSpace = "nowrap";
field.append(deviceLabel);
const actionDiv = document.createElement("div");
const configBtn = document.createElement("img");
configBtn.classList.add("clickable");
configBtn.src = "images/actions/device/config.svg";
configBtn.title = "Config Device";
configBtn.addEventListener("click", handleDeviceConfig);
configBtn.dataset.device = deviceID;
configBtn.dataset.values = deviceDetails;
configBtn.dataset.name = deviceName;
actionDiv.appendChild(configBtn);
const deleteBtn = document.createElement("img");
deleteBtn.classList.add("clickable");
deleteBtn.src = "images/actions/delete-active.svg";
deleteBtn.title = "Delete Device";
deleteBtn.addEventListener("click", handleDeviceDelete);
deleteBtn.dataset.device = deviceID;
deleteBtn.dataset.values = deviceDetails;
deleteBtn.dataset.name = deviceName;
actionDiv.appendChild(deleteBtn);
field.append(actionDiv);
}
async function handleDeviceConfig () {
const deviceID = this.dataset.device;
const deviceDetails = this.dataset.values;
const deviceName = this.dataset.name;
const header = `Edit Expansion Card ${deviceID}`;
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") {
document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg";
const body = {
device: form.get("device"),
pcie: form.get("pcie") ? 1 : 0
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${deviceID}/modify`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDevices();
}
});
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
d.querySelector("#device").append(new Option(deviceName, deviceDetails.split(",")[0]));
for (const availDevice of availDevices) {
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.id));
}
d.querySelector("#pcie").checked = deviceDetails.includes("pcie=1");
}
async function handleDeviceDelete () {
const deviceID = this.dataset.device;
const header = `Remove Expansion Card ${deviceID}`;
const body = "";
dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${deviceID}/delete`, "DELETE");
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDevices();
}
});
}
async function handleDeviceAdd () {
const header = "Add Expansion Card";
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") {
const body = {
device: form.get("device"),
pcie: form.get("pcie") ? 1 : 0
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/create`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDevices();
}
});
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
for (const availDevice of availDevices) {
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.id));
}
d.querySelector("#pcie").checked = true;
}
async function populateBoot () {
if (type === "qemu") {
document.querySelector("#boot-card").classList.remove("none");
document.querySelector("#enabled").title = "Enabled";
document.querySelector("#disabled").title = "Disabled";
let order = [];
if (config.data.boot.startsWith("order=")) {
order = config.data.boot.replace("order=", "").split(";");
}
const bootable = { disabled: [] };
const eligible = bootMetaData.eligiblePrefixes;
for (let i = 0; i < order.length; i++) {
const element = order[i];
const prefix = eligible.find((pref) => order[i].startsWith(pref));
const detail = config.data[element];
bootable[i] = { id: element, value: element, prefix, detail };
}
Object.keys(config.data).forEach((element) => {
const prefix = eligible.find((pref) => element.startsWith(pref));
const detail = config.data[element];
if (prefix && !order.includes(element)) {
bootable.disabled.push({ id: element, value: element, prefix, detail });
}
});
Object.keys(bootable).sort();
Object.keys(bootable).forEach((element) => {
if (element !== "disabled") {
addBootLine("enabled", bootable[element], document.querySelector("#enabled-spacer"));
}
else {
bootable.disabled.forEach((item) => {
addBootLine("disabled", item, document.querySelector("#disabled-spacer"));
});
}
});
}
}
function addBootLine (container, data, before = null) {
const item = document.createElement("draggable-item");
item.data = data;
item.innerHTML = `
<div style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 10px; align-items: center;">
<img src="images/actions/drag.svg" id="drag" alt="drag icon">
<img src="${bootMetaData[data.prefix].icon}" alt="${bootMetaData[data.prefix].alt}">
<p style="margin: 0px;">${data.id}</p>
<p style="margin: 0px; overflow-x: hidden; white-space: nowrap;">${data.detail}</p>
</div>
`;
item.draggable = true;
item.classList.add("drop-target");
item.id = `boot-${data.id}`;
if (before) {
document.querySelector(`#${container}`).insertBefore(item, before);
}
else {
document.querySelector(`#${container}`).append(item);
}
item.container = container;
item.value = data.value;
}
function deleteBootLine (id) {
const query = `#${id}`;
const enabled = document.querySelector("#enabled");
const disabled = document.querySelector("#disabled");
const inEnabled = enabled.querySelector(query);
const inDisabled = disabled.querySelector(query);
if (inEnabled) {
enabled.removeChild(inEnabled);
}
if (inDisabled) {
disabled.removeChild(inDisabled);
}
}
function updateBootLine (id, newData) {
const enabled = document.querySelector("#enabled");
const disabled = document.querySelector("#disabled");
let element = null;
if (enabled.querySelector(`#${id}`)) {
element = enabled.querySelector(`#${id}`);
}
if (disabled.querySelector(`#${id}`)) {
element = disabled.querySelector(`#${id}`);
}
if (element) {
const container = element.container;
const before = element.nextSibling;
deleteBootLine(id);
addBootLine(container, newData, before);
return true;
}
else {
return false;
}
}
async function handleFormExit () {
const body = {
cores: document.querySelector("#cores").value,
memory: document.querySelector("#ram").value
};
if (type === "lxc") {
body.swap = document.querySelector("#swap").value;
}
else if (type === "qemu") {
body.proctype = document.querySelector("#proctype").value;
body.boot = document.querySelector("#enabled").value;
}
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/resources`, "POST", body);
if (result.status === 200) {
await getConfig();
populateDisk();
goToPage("index.html");
}
else {
alert(result.error);
}
}

View File

@ -1,33 +1,39 @@
const blank = document.createElement("img"); // Map valid UUIDs used by draggable-item elements in order to better validate data transfers to ignore random data transfers.
const draggableItemUUIDs = {};
/**
* Get the data transfer source object by parsing its types. Valid draggable-item events have one type of the format `application/json/${uuid}`.
* The function takes the entire type list from event.dataTransfer.types and returns the source object if valid, or null if invalid.
* @param {*} typesList from event.dataTransfer.types
* @returns {Object} Object containing the type, uuid, and element of the dataTransfer source or null
*/
function getDragSource (typesList) {
if (typesList.length !== 1) {
return null;
}
const typeString = typesList[0];
const type = typeString.split("/");
if (type.length === 3 && type[0] === "application" && type[1] === "json" && draggableItemUUIDs[type[2]]) {
return { type: typeString, uuid: type[2], element: draggableItemUUIDs[type[2]] };
}
else {
return null;
}
}
class DraggableContainer extends HTMLElement { class DraggableContainer extends HTMLElement {
constructor () { constructor () {
super(); super();
this.attachShadow({ mode: "open" }); this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style>
draggable-item.ghost::part(wrapper) {
border: 1px dashed var(--main-text-color);
border-radius: 5px;
margin: -1px;
}
draggable-item::part(wrapper) {
cursor: grab;
}
</style>
<label id="title"></label> <label id="title"></label>
<div id="wrapper" style="padding-bottom: 1em;"></div> <div id="wrapper">
<draggable-item id="bottom" class="drop-target"></draggable-item>
</div>
`; `;
this.content = this.shadowRoot.querySelector("#wrapper"); this.content = this.shadowRoot.querySelector("#wrapper");
this.bottom = this.shadowRoot.querySelector("#bottom");
this.titleElem = this.shadowRoot.querySelector("#title"); this.titleElem = this.shadowRoot.querySelector("#title");
window.Sortable.create(this.content, {
group: "boot",
ghostClass: "ghost",
setData: function (dataTransfer, dragEl) {
dataTransfer.setDragImage(blank, 0, 0);
}
});
} }
get title () { get title () {
@ -39,10 +45,14 @@ class DraggableContainer extends HTMLElement {
} }
append (newNode) { append (newNode) {
this.content.appendChild(newNode, this.bottom); newNode.uuid = window.crypto.randomUUID();
draggableItemUUIDs[newNode.uuid] = newNode;
this.content.insertBefore(newNode, this.bottom);
} }
insertBefore (newNode, referenceNode) { insertBefore (newNode, referenceNode) {
newNode.uuid = window.crypto.randomUUID();
draggableItemUUIDs[newNode.uuid] = newNode;
this.content.insertBefore(newNode, referenceNode); this.content.insertBefore(newNode, referenceNode);
} }
@ -52,6 +62,7 @@ class DraggableContainer extends HTMLElement {
removeChild (node) { removeChild (node) {
if (node && this.content.contains(node)) { if (node && this.content.contains(node)) {
draggableItemUUIDs[node.uuid] = null;
this.content.removeChild(node); this.content.removeChild(node);
return true; return true;
} }
@ -74,7 +85,6 @@ class DraggableContainer extends HTMLElement {
} }
class DraggableItem extends HTMLElement { class DraggableItem extends HTMLElement {
#value = null;
uuid = null; uuid = null;
constructor () { constructor () {
super(); super();
@ -82,19 +92,80 @@ class DraggableItem extends HTMLElement {
// for whatever reason, only grid layout seems to respect the parent's content bounds // for whatever reason, only grid layout seems to respect the parent's content bounds
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>
img, svg { #drag-over {
height: 1.5em;
border: 1px dotted var(--main-text-color);
border-radius: 5px;
background-color: rgba(0,0,0,0.25);
}
img {
height: 1em; height: 1em;
width: 1em; width: 1em;
} }
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
</style> </style>
<div id="wrapper" part="wrapper"></div> <div id="drag-over" style="display: none;"></div>
<div id="wrapper">
<div style="min-height: 1.5em;"></div>
</div>
`; `;
this.content = this.shadowRoot.querySelector("#wrapper"); this.content = this.shadowRoot.querySelector("#wrapper");
// add drag and drop listeners
this.addEventListener("dragstart", (event) => {
this.content.style.opacity = "0.5";
const data = { id: this.id, data: this.data, content: this.content.innerHTML, value: this.value };
event.dataTransfer.setData(`application/json/${this.uuid}`, JSON.stringify(data));
event.dataTransfer.effectAllowed = "move";
const blank = document.createElement("img");
event.dataTransfer.setDragImage(blank, 0, 0);
setTimeout(() => {
this.content.style.visibility = "hidden";
this.content.style.height = "0";
}, 0);
});
this.addEventListener("dragend", (event) => {
if (event.dataTransfer.dropEffect === "move") {
this.parentElement.removeChild(this);
}
else {
this.content.attributeStyleMap.clear();
}
});
this.addEventListener("dragenter", (event) => {
const sourceElement = getDragSource(event.dataTransfer.types);
if (event.target.dropTarget && sourceElement) {
event.target.dragOver = sourceElement.element.innerHTML;
}
event.preventDefault();
});
this.addEventListener("dragleave", (event) => {
if (event.target.dragOver && getDragSource(event.dataTransfer.types)) {
event.target.dragOver = false;
}
event.preventDefault();
});
this.addEventListener("dragover", (event) => {
event.preventDefault();
});
this.addEventListener("drop", (event) => {
if (event.target.dragOver) {
event.target.dragOver = false;
}
const sourceElement = getDragSource(event.dataTransfer.types);
if (event.target.dropTarget && sourceElement) {
const transfer = JSON.parse(event.dataTransfer.getData(sourceElement.type));
const item = document.createElement("draggable-item");
item.data = transfer.data;
item.innerHTML = transfer.content;
item.draggable = true;
item.dropTarget = true;
item.id = transfer.id;
item.value = transfer.data.value;
item.uuid = sourceElement.uuid;
event.target.parentElement.insertBefore(item, event.target);
}
this.content.attributeStyleMap.clear();
event.preventDefault();
});
} }
get innerHTML () { get innerHTML () {
@ -105,12 +176,34 @@ class DraggableItem extends HTMLElement {
this.content.innerHTML = innerHTML; this.content.innerHTML = innerHTML;
} }
get value () { get dropTarget () {
return this.#value; return this.classList.contains("drop-target");
} }
set value (value) { set dropTarget (dropTarget) {
this.#value = value; if (dropTarget) {
this.classList.add("drop-target");
}
else {
this.classList.remove("drop-target");
}
}
get dragOver () {
return this.classList.contains("drag-over");
}
set dragOver (dragOver) {
if (dragOver) {
this.classList.add("drag-over");
this.shadowRoot.querySelector("#drag-over").style.display = "block";
this.shadowRoot.querySelector("#drag-over").innerHTML = dragOver;
}
else {
this.classList.remove("drag-over");
this.shadowRoot.querySelector("#drag-over").style.display = "none";
this.shadowRoot.querySelector("#drag-over").innerHTML = "";
}
} }
} }

View File

@ -1,278 +1,19 @@
import { requestPVE, requestAPI, goToPage, setTitleAndHeader, setAppearance, getSearchSettings, goToURL, instancesConfig, nodesConfig, setSVGSrc, setSVGAlt } from "./utils.js"; import { requestPVE, requestAPI, goToPage, setTitleAndHeader } from "./utils.js";
import { alert, dialog } from "./dialog.js"; import { alert, dialog } from "./dialog.js";
import { setupClientSync } from "./clientsync.js"; import { setupClientSync } from "./clientsync.js";
import wfaInit from "../modules/wfa.js"; import wf_align from "../modules/wfa.js";
import { PVE } from "../vars.js";
class InstanceCard 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;">
<hr class="w3-show-small w3-hide-medium w3-hide-large" style="margin: 0; margin-bottom: 1em;">
<p class="w3-col l1 m2 s6" id="instance-id"></p>
<p class="w3-col l2 m3 s6" id="instance-name"></p>
<p class="w3-col l1 m2 w3-hide-small" id="instance-type"></p>
<div class="w3-col l2 m3 s6 flex row nowrap">
<svg id="instance-status-icon"></svg>
<p id="instance-status"></p>
</div>
<p class="w3-col l2 w3-hide-medium w3-hide-small" id="node-name"></p>
<div class="w3-col l2 w3-hide-medium w3-hide-small flex row nowrap">
<svg id="node-status-icon"></svg>
<p id="node-status"></p>
</div>
<div class="w3-col l2 m2 s6 flex row nowrap" style="height: 1lh;">
<svg id="power-btn" tabindex="0" role="button"></svg>
<svg id="console-btn" tabindex="0" role="button"></svg>
<svg id="configure-btn" tabindex="0" role="button"></svg>
<svg id="delete-btn" tabindex="0" role="button"></svg>
</div>
</div>
`;
this.actionLock = false;
}
get data () {
return {
type: this.type,
status: this.status,
vmid: this.status,
name: this.name,
node: this.node,
searchQuery: this.searchQuery
};
}
set data (data) {
if (data.status === "unknown") {
data.status = "stopped";
}
this.type = data.type;
this.status = data.status;
this.vmid = data.vmid;
this.name = data.name;
this.node = data.node;
this.searchQueryResult = data.searchQueryResult;
this.update();
}
update () {
const vmidParagraph = this.shadowRoot.querySelector("#instance-id");
vmidParagraph.innerText = this.vmid;
const nameParagraph = this.shadowRoot.querySelector("#instance-name");
if (this.searchQueryResult.alignment) {
let i = 0; // name index
let c = 0; // alignment index
const alignment = this.searchQueryResult.alignment;
while (i < this.name.length && c < alignment.length) {
if (alignment[c] === "M") {
const part = document.createElement("span");
part.innerText = this.name[i];
part.style = "color: var(--lightbg-text-color); background-color: var(--highlight-color);";
nameParagraph.append(part);
i++;
c++;
}
else if (alignment[c] === "I") {
const part = document.createElement("span");
part.innerText = this.name[i];
nameParagraph.append(part);
i++;
c++;
}
else if (alignment[c] === "D") {
c++;
}
else if (alignment[c] === "X") {
const part = document.createElement("span");
part.innerText = this.name[i];
nameParagraph.append(part);
i++;
c++;
}
}
}
else {
nameParagraph.innerHTML = this.name ? this.name : "&nbsp;";
}
const typeParagraph = this.shadowRoot.querySelector("#instance-type");
typeParagraph.innerText = this.type;
const statusParagraph = this.shadowRoot.querySelector("#instance-status");
statusParagraph.innerText = this.status;
const statusIcon = this.shadowRoot.querySelector("#instance-status-icon");
setSVGSrc(statusIcon, instancesConfig[this.status].status.src);
setSVGAlt(statusIcon, instancesConfig[this.status].status.alt);
const nodeNameParagraph = this.shadowRoot.querySelector("#node-name");
nodeNameParagraph.innerText = this.node.name;
const nodeStatusParagraph = this.shadowRoot.querySelector("#node-status");
nodeStatusParagraph.innerText = this.node.status;
const nodeStatusIcon = this.shadowRoot.querySelector("#node-status-icon");
setSVGSrc(nodeStatusIcon, nodesConfig[this.node.status].status.src);
setSVGAlt(nodeStatusIcon, nodesConfig[this.node.status].status.alt);
const powerButton = this.shadowRoot.querySelector("#power-btn");
setSVGSrc(powerButton, instancesConfig[this.status].power.src);
setSVGAlt(powerButton, instancesConfig[this.status].power.alt);
if (instancesConfig[this.status].power.clickable) {
powerButton.classList.add("clickable");
powerButton.onclick = this.handlePowerButton.bind(this);
}
const configButton = this.shadowRoot.querySelector("#configure-btn");
setSVGSrc(configButton, instancesConfig[this.status].config.src);
setSVGAlt(configButton, instancesConfig[this.status].config.alt);
if (instancesConfig[this.status].config.clickable) {
configButton.classList.add("clickable");
configButton.onclick = this.handleConfigButton.bind(this);
}
const consoleButton = this.shadowRoot.querySelector("#console-btn");
setSVGSrc(consoleButton, instancesConfig[this.status].console.src);
setSVGAlt(consoleButton, instancesConfig[this.status].console.alt);
if (instancesConfig[this.status].console.clickable) {
consoleButton.classList.add("clickable");
consoleButton.onclick = this.handleConsoleButton.bind(this);
}
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
setSVGSrc(deleteButton, instancesConfig[this.status].delete.src);
setSVGAlt(deleteButton, instancesConfig[this.status].delete.alt);
if (instancesConfig[this.status].delete.clickable) {
deleteButton.classList.add("clickable");
deleteButton.onclick = this.handleDeleteButton.bind(this);
}
if (this.node.status !== "online") {
powerButton.classList.add("hidden");
configButton.classList.add("hidden");
consoleButton.classList.add("hidden");
deleteButton.classList.add("hidden");
}
}
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 ${this.vmid}</p>`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
this.actionLock = true;
const targetAction = this.status === "running" ? "stop" : "start";
const targetStatus = this.status === "running" ? "stopped" : "running";
const prevStatus = this.status;
this.status = "loading";
this.update();
const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid });
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
while (true) {
const taskStatus = await requestPVE(`/nodes/${this.node.name}/tasks/${result.data}/status`, "GET");
if (taskStatus.data.status === "stopped" && taskStatus.data.exitstatus === "OK") { // task stopped and was successful
this.status = targetStatus;
this.update();
this.actionLock = false;
break;
}
else if (taskStatus.data.status === "stopped") { // task stopped but was not successful
this.status = prevStatus;
alert(`attempted to ${targetAction} ${this.vmid} but process returned stopped:${result.data.exitstatus}`);
this.update();
this.actionLock = false;
break;
}
else { // task has not stopped
await waitFor(1000);
}
}
}
});
}
}
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("instance.html", { node: this.node.name, type: this.type, vmid: this.vmid });
}
}
handleConsoleButton () {
if (!this.actionLock && this.status === "running") {
const data = { console: `${this.type === "qemu" ? "kvm" : "lxc"}`, vmid: this.vmid, vmname: this.name, node: this.node.name, resize: "off", cmd: "" };
data[`${this.type === "qemu" ? "novnc" : "xtermjs"}`] = 1;
goToURL(PVE, data, true);
}
}
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 ${this.vmid}</p>`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
this.actionLock = true;
this.status = "loading";
this.update();
const action = {};
action.purge = 1;
action["destroy-unreferenced-disks"] = 1;
const result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE");
if (result.status === 200) {
if (this.parentElement) {
this.parentElement.removeChild(this);
}
}
else {
alert(result.error);
this.status = this.prevStatus;
this.update();
this.actionLock = false;
}
}
});
}
}
}
customElements.define("instance-card", InstanceCard);
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
let instances = []; let instances = [];
async function init () { async function init () {
setAppearance();
setTitleAndHeader(); setTitleAndHeader();
const cookie = document.cookie; const cookie = document.cookie;
if (cookie === "") { if (cookie === "") {
goToPage("login.html"); goToPage("login.html");
} }
wfaInit("modules/wfa.wasm");
document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd); document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd);
document.querySelector("#vm-search").addEventListener("input", populateInstances); document.querySelector("#vm-search").addEventListener("input", populateInstances);
@ -298,27 +39,33 @@ async function getInstances () {
} }
async function populateInstances () { async function populateInstances () {
const searchCriteria = getSearchSettings(); let searchCriteria = localStorage.getItem("search-criteria");
if (!searchCriteria) {
searchCriteria = "fuzzy";
localStorage.setItem("search-criteria", "fuzzy");
}
const searchQuery = document.querySelector("#search").value || null; const searchQuery = document.querySelector("#search").value || null;
let criteria; let criteria;
if (!searchQuery) { if (!searchQuery) {
criteria = (item, query = null) => { criteria = (a, b) => {
return { score: item.vmid, alignment: null }; return (a.vmid > b.vmid) ? 1 : -1;
}; };
} }
else if (searchCriteria === "exact") { else if (searchCriteria === "exact") {
criteria = (item, query) => { criteria = (a, b) => {
const substrInc = item.includes(query); const aInc = a.name.toLowerCase().includes(searchQuery.toLowerCase());
if (substrInc) { const bInc = b.name.toLowerCase().includes(searchQuery.toLowerCase());
const substrStartIndex = item.indexOf(query); if (aInc && bInc) {
const queryLength = query.length; return a.vmid > b.vmid ? 1 : -1;
const remaining = item.length - substrInc - queryLength; }
const alignment = `${"X".repeat(substrStartIndex)}${"M".repeat(queryLength)}${"X".repeat(remaining)}`; else if (aInc && !bInc) {
return { score: 1, alignment }; return -1;
}
else if (!aInc && bInc) {
return 1;
} }
else { else {
const alignment = `${"X".repeat(item.length)}`; return a.vmid > b.vmid ? 1 : -1;
return { score: 0, alignment };
} }
}; };
} }
@ -326,44 +73,34 @@ async function populateInstances () {
const penalties = { const penalties = {
m: 0, m: 0,
x: 1, x: 1,
o: 0, o: 1,
e: 1 e: 1
}; };
criteria = (item, query) => { criteria = (a, b) => {
// lower is better // lower is better
const { score, CIGAR } = global.wfAlign(query, item, penalties, true); const aAlign = wf_align(a.name.toLowerCase(), searchQuery.toLowerCase(), penalties);
const alignment = global.DecodeCIGAR(CIGAR); const aScore = aAlign.score / a.name.length;
return { score: score / item.length, alignment }; 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;
}
}; };
} }
sortInstances(criteria, searchQuery); instances.sort(criteria);
const instanceContainer = document.querySelector("#instance-container"); const instanceContainer = document.querySelector("#instance-container");
instanceContainer.innerHTML = ""; instanceContainer.innerHTML = "";
for (let i = 0; i < instances.length; i++) { for (let i = 0; i < instances.length; i++) {
const newInstance = document.createElement("instance-card"); const newInstance = document.createElement("instance-card");
instances[i].searchQuery = searchQuery;
newInstance.data = instances[i]; newInstance.data = instances[i];
instanceContainer.append(newInstance); instanceContainer.append(newInstance);
} }
} }
function sortInstances (criteria, searchQuery) {
for (let i = 0; i < instances.length; i++) {
const { score, alignment } = criteria(instances[i].name.toLowerCase(), searchQuery ? searchQuery.toLowerCase() : "");
instances[i].searchQueryResult = { score, alignment };
}
const sortCriteria = (a, b) => {
const aScore = a.searchQueryResult.score;
const bScore = b.searchQueryResult.score;
if (aScore === bScore) {
return a.vmid > b.vmid ? 1 : -1;
}
else {
return aScore - bScore;
}
};
instances.sort(sortCriteria);
}
async function handleInstanceAdd () { async function handleInstanceAdd () {
const header = "Create New Instance"; const header = "Create New Instance";
@ -377,28 +114,26 @@ async function handleInstanceAdd () {
<label for="node">Node</label> <label for="node">Node</label>
<select class="w3-select w3-border" name="node" id="node" required></select> <select class="w3-select w3-border" name="node" id="node" required></select>
<label for="name">Name</label> <label for="name">Name</label>
<input class="w3-input w3-border" name="name" id="name" required> <input class="w3-input w3-border" name="name" id="name" required></input>
<label for="vmid">ID</label> <label for="vmid">ID</label>
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required> <input class="w3-input w3-border" name="vmid" id="vmid" type="number" required></input>
<label for="pool">Pool</label> <label for="pool">Pool</label>
<select class="w3-select w3-border" name="pool" id="pool" required></select> <select class="w3-select w3-border" name="pool" id="pool" required></select>
<label for="cores">Cores (Threads)</label> <label for="cores">Cores (Threads)</label>
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required> <input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required></input>
<label for="memory">Memory (MiB)</label> <label for="memory">Memory (MiB)</label>
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16", step="1" required> <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> <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> <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 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> <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> <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> <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> <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> <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 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> <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 class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled></input>
<label for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" required disabled>
</form> </form>
`; `;
@ -510,14 +245,4 @@ async function handleInstanceAdd () {
templateImage.append(new Option(template.name, template.volid)); templateImage.append(new Option(template.name, template.volid));
} }
templateImage.selectedIndex = -1; templateImage.selectedIndex = -1;
const password = d.querySelector("#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);
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,11 @@
import { goToPage, requestPVE, setTitleAndHeader, setAppearance, requestAPI } from "./utils.js"; import { requestTicket, goToPage, deleteAllCookies, requestPVE, setTitleAndHeader } from "./utils.js";
import { alert } from "./dialog.js"; import { alert } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
async function init () { async function init () {
await deleteAllCookies();
setAppearance();
setTitleAndHeader(); setTitleAndHeader();
await deleteAllCookies();
const formSubmitButton = document.querySelector("#submit"); const formSubmitButton = document.querySelector("#submit");
const realms = await requestPVE("/access/domains", "GET"); const realms = await requestPVE("/access/domains", "GET");
const realmSelect = document.querySelector("#realm"); const realmSelect = document.querySelector("#realm");
@ -42,12 +41,3 @@ async function init () {
} }
}); });
} }
async function requestTicket (username, password, realm) {
const response = await requestAPI("/access/ticket", "POST", { username: `${username}@${realm}`, password }, false);
return response;
}
async function deleteAllCookies () {
await requestAPI("/access/ticket", "DELETE");
}

View File

@ -1,9 +1,8 @@
import { setTitleAndHeader, setAppearance } from "./utils.js"; import { setTitleAndHeader } from "./utils.js";
window.addEventListener("DOMContentLoaded", init); window.addEventListener("DOMContentLoaded", init);
function init () { function init () {
setAppearance();
setTitleAndHeader(); setTitleAndHeader();
const scheme = localStorage.getItem("sync-scheme"); const scheme = localStorage.getItem("sync-scheme");
if (scheme) { if (scheme) {
@ -17,10 +16,6 @@ function init () {
if (search) { if (search) {
document.querySelector(`#search-${search}`).checked = true; document.querySelector(`#search-${search}`).checked = true;
} }
const theme = localStorage.getItem("appearance-theme");
if (theme) {
document.querySelector("#appearance-theme").value = theme;
}
document.querySelector("#settings").addEventListener("submit", handleSaveSettings, false); document.querySelector("#settings").addEventListener("submit", handleSaveSettings, false);
} }
@ -30,6 +25,5 @@ function handleSaveSettings (event) {
localStorage.setItem("sync-scheme", form.get("sync-scheme")); localStorage.setItem("sync-scheme", form.get("sync-scheme"));
localStorage.setItem("sync-rate", form.get("sync-rate")); localStorage.setItem("sync-rate", form.get("sync-rate"));
localStorage.setItem("search-criteria", form.get("search-criteria")); localStorage.setItem("search-criteria", form.get("search-criteria"));
localStorage.setItem("appearance-theme", form.get("appearance-theme"));
window.location.reload(); window.location.reload();
} }

View File

@ -1,681 +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 resourceRulesLines = {}; // list of all resource rules fieldsets on the page
const resourceInputTypes = { // input types for each resource for config page
cpu: {
type: "list",
element: "interactive-list",
align: "start"
},
cores: {
type: "numeric",
element: "input",
attributes: {
type: "number"
}
},
memory: {
type: "numeric",
element: "input",
attributes: {
type: "number"
}
},
swap: {
type: "numeric",
element: "input",
attributes: {
type: "number"
}
},
network: {
type: "numeric",
element: "input",
attributes: {
type: "number"
}
},
storage: {
type: "numeric",
icon: "images/resources/disk.svg",
element: "input",
unitText: "B",
attributes: {
type: "number"
}
},
pci: {
type: "list",
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 () {
const ret = [];
for (const elem of this.container.childNodes) {
ret.push(elem.value);
}
return ret;
}
set value (value) {
this.container.innerHTML = "";
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">
<link rel="stylesheet" href="css/form.css">
<style>
#container {
text-align: left;
display: grid;
}
p {
padding: 0;
margin: 0;
white-space: nowrap;
overflow: hidden;
}
svg {
margin-top: calc(-0.5em + 0.5lh);
}
@media (width >= 993px) {
#container {
grid-template-columns: 15% 1fr 15% auto;
}
}
@media (width <= 993px) and (width >= 601px) {
#container {
grid-template-columns: 1fr 15% auto;
}
}
@media (width <= 601px) {
#container {
grid-template-columns: 1fr auto;
}
}
</style>
<div id="container">
<p class="w3-hide-medium w3-hide-small" id="name"></p>
<p id="match"></p>
<p class="w3-hide-small" id="max"></p>
<div>
<svg id="config-btn" class="clickable" tabindex="0" role="button"><use></use></svg>
<svg id="delete-btn" class="clickable" tabindex="0" role="button"><use></use></svg>
</div>
</div>
`;
this.#nameElem = this.shadowRoot.querySelector("#name");
this.#matchElem = this.shadowRoot.querySelector("#match");
this.#maxElem = this.shadowRoot.querySelector("#max");
this.configBtn = this.shadowRoot.querySelector("#config-btn");
this.configBtn.onclick = this.#handleConfig.bind(this);
setSVGSrc(this.configBtn, "images/common/config.svg");
setSVGAlt(this.configBtn, "Config Item");
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 = `match="${this.#match}"`;
this.#maxElem.innerText = `max=${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();
}
get value () {
return {
name: this.#name,
match: this.#match,
max: this.#max
};
}
set value (value) {
this.#name = value.name;
this.#match = value.match;
this.#max = value.max;
this.#update();
}
#handleConfig () {
const header = `Edit ${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>
`;
const d = dialog(header, body, async (result, form) => {
if (result === "confirm") {
const newItem = {
name: form.get("name"),
match: form.get("match"),
max: form.get("max")
};
this.value = newItem;
}
});
d.querySelector("#name").value = this.#name;
d.querySelector("#match").value = this.#match;
d.querySelector("#max").value = this.#max;
}
#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.value = 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];
let resourceLine;
let resourceConfig;
if (resourcesConfigPage[resourceName]) {
resourceConfig = resourcesConfigPage[resourceName];
resourceConfig.id = `${resourceName}-global`;
if (resourceConfig.type === "list") {
resourceLine = addResourceLine(resourceConfig, field, { value: resource.global }, "(Global)");
}
else {
resourceLine = addResourceLine(resourceConfig, field, { 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
resourceConfig.id = `${resourceName}-${nodeSpecificName}`;
if (resourceConfig.type === "list") {
resourceLine = addResourceLine(resourceConfig, field, { value: resource.nodes[nodeSpecificName] }, `(${nodeSpecificName})`);
}
else {
resourceLine = addResourceLine(resourceConfig, field, { value: resource.nodes[nodeSpecificName].max }, `(${nodeSpecificName})`);
}
postPopulateResourceLine(field, resourceName, nodeSpecificName, resourceConfig, resourceLine);
}
}
else {
resourceConfig = resourcesConfigPage.storage;
resourceConfig.id = `${resourceName}-global`;
resourceConfig.name = resourceName;
resourceLine = addResourceLine(resourceConfig, field, { 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
resourceConfig.id = `${resourceName}-${nodeSpecificName}`;
resourceLine = addResourceLine(resourceConfig, field, { 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;
resourceLine.resourceType = resourceConfig.type;
resourceRulesLines[resourceLine.element.id] = resourceLine;
}
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");
// 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");
const resourceConfig = resourcesConfigPage[name];
let resourceLine;
if (scope === "global" && type === "numeric") {
userData.resources[name].global = { max: 0 };
resourceLine = addResourceLine(resourceConfig, field, { value: userData.resources[name].global.max }, "(Global)");
}
else if (scope === "global" && type === "list") {
userData.resources[name].global = [];
resourceLine = addResourceLine(resourceConfig, field, { value: userData.resources[name].global }, "(Global)");
}
else if (scope !== "global" && type === "numeric") {
userData.resources[name].nodes[scope] = { max: 0 };
resourceLine = addResourceLine(resourceConfig, field, { value: userData.resources[name].nodes[scope].max }, `(${scope})`);
}
else if (scope !== "global" && type === "list") {
userData.resources[name].nodes[scope] = [];
resourceLine = addResourceLine(resourceConfig, field, { 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;
}
delete resourceRulesLines[this.element.id];
}
});
}
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.value = 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.value = 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 () {
const body = {
attributes: {
memberOf: []
},
resources: {},
cluster: {
admin: document.querySelector("#admin").checked,
nodes: {},
pools: {},
vmid: {
min: document.querySelector("#vmid-min").value,
max: document.querySelector("#vmid-max").value
}
}
};
for (const group of document.querySelector("#groups-enabled").value) {
body.attributes.memberOf.push(group.dn);
}
// populate resources
for (const key of Object.keys(resourceRulesLines)) {
const resourceLine = resourceRulesLines[key];
// if type is numeric
if (resourceLine.resourceType === "numeric") {
if (body.resources[resourceLine.resourceName] === undefined) {
body.resources[resourceLine.resourceName] = {
global: {
max: 0
},
nodes: {}
};
}
if (resourceLine.resourceScope === "global") {
body.resources[resourceLine.resourceName].global.max = resourceLine.element.value;
}
else {
body.resources[resourceLine.resourceName].nodes[resourceLine.resourceScope].max = resourceLine.element.value;
}
}
else {
if (body.resources[resourceLine.resourceName] === undefined) {
body.resources[resourceLine.resourceName] = {
global: [],
nodes: {}
};
}
if (resourceLine.resourceScope === "global") {
body.resources[resourceLine.resourceName].global = resourceLine.element.value;
}
else {
body.resources[resourceLine.resourceName].nodes[resourceLine.resourceScope] = resourceLine.element.value;
}
}
}
// populate nodes
for (const node of document.querySelector("#nodes-enabled").value) {
body.cluster.nodes[node] = true;
}
// populate pools
for (const pool of document.querySelector("#pools-enabled").value) {
body.cluster.pools[pool] = true;
}
// TODO post to api
console.log(body);
}

View File

@ -1,30 +1,6 @@
import { API, organization } from "../vars.js"; import { API, organization } from "../vars.js";
export const resourcesConfig = { export const resourcesConfig = {
cpu: {
name: "CPU Type",
icon: "images/resources/cpu.svg",
id: "proctype",
unitText: null
},
cores: {
name: "CPU Amount",
icon: "images/resources/cpu.svg",
id: "cores",
unitText: "Cores"
},
memory: {
name: "Memory",
icon: "images/resources/ram.svg",
id: "ram",
unitText: "MiB"
},
swap: {
name: "Swap",
icon: "images/resources/swap.svg",
id: "swap",
unitText: "MiB"
},
disk: { disk: {
actionBarOrder: ["move", "resize", "detach_attach", "delete"], actionBarOrder: ["move", "resize", "detach_attach", "delete"],
lxc: { lxc: {
@ -38,31 +14,12 @@ export const resourcesConfig = {
ide: { name: "IDE", icon: "images/resources/disk.svg", actions: ["delete"] }, ide: { name: "IDE", icon: "images/resources/disk.svg", actions: ["delete"] },
sata: { name: "SATA", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] }, sata: { name: "SATA", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] },
unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] } unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] }
},
actions: {
attach: {
src: "images/actions/disk/attach.svg",
title: "Attach Disk"
},
detach: {
src: "images/actions/disk/detach.svg",
title: "Detach Disk"
},
delete: null
} }
}, },
network: { network: {
name: "Network",
icon: "images/resources/network.svg",
id: "network",
unitText: "MB/s",
prefix: "net" prefix: "net"
}, },
pci: { pcie: {
name: "Devices",
icon: "images/resources/device.svg",
id: "devices",
unitText: null,
prefix: "hostpci" prefix: "hostpci"
} }
}; };
@ -204,6 +161,11 @@ export function getCookie (cname) {
return ""; return "";
} }
export async function requestTicket (username, password, realm) {
const response = await requestAPI("/auth/ticket", "POST", { username: `${username}@${realm}`, password }, false);
return response;
}
export async function requestPVE (path, method, body = null) { export async function requestPVE (path, method, body = null) {
const prms = new URLSearchParams(body); const prms = new URLSearchParams(body);
const content = { const content = {
@ -297,210 +259,11 @@ export function getURIData () {
return Object.fromEntries(url.searchParams); return Object.fromEntries(url.searchParams);
} }
export async function setTitleAndHeader () { export async function deleteAllCookies () {
await requestAPI("/auth/ticket", "DELETE");
}
export function setTitleAndHeader () {
document.title = `${organization} - dashboard`; document.title = `${organization} - dashboard`;
document.querySelector("h1").innerText = organization; 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 = {
"sync-scheme": "always",
"sync-rate": 5,
"search-criteria": "fuzzy",
"appearance-theme": "auto"
};
export function getSyncSettings () {
let scheme = localStorage.getItem("sync-scheme");
let rate = Number(localStorage.getItem("sync-rate"));
if (!scheme) {
scheme = settingsDefault["sync-scheme"];
localStorage.setItem("sync-scheme", scheme);
}
if (!rate) {
rate = settingsDefault["sync-rate"];
localStorage.setItem("sync-rate", rate);
}
return { scheme, rate };
}
export function getSearchSettings () {
let searchCriteria = localStorage.getItem("search-criteria");
if (!searchCriteria) {
searchCriteria = settingsDefault["search-criteria"];
localStorage.setItem("search-criteria", searchCriteria);
}
return searchCriteria;
}
export function setAppearance () {
let theme = localStorage.getItem("appearance-theme");
if (!theme) {
theme = settingsDefault["appearance-theme"];
localStorage.setItem("appearance-theme", theme);
}
if (theme === "auto") {
document.querySelector(":root").classList.remove("dark-theme", "light-theme");
}
else if (theme === "dark") {
document.querySelector(":root").classList.remove("light-theme");
document.querySelector(":root").classList.add("dark-theme");
}
else if (theme === "light") {
document.querySelector(":root").classList.add("light-theme");
document.querySelector(":root").classList.remove("dark-theme");
}
}
// assumes href is path to svg, and id to grab is #symb
export function setSVGSrc (svgElem, href) {
let useElem = svgElem.querySelector("use");
if (!useElem) {
useElem = document.createElementNS("http://www.w3.org/2000/svg", "use");
}
useElem.setAttribute("href", `${href}#symb`);
svgElem.append(useElem);
}
export function setSVGAlt (svgElem, alt) {
svgElem.setAttribute("aria-label", alt);
}
/**
* Simple object check.
* @param item
* @returns {boolean}
*/
export function isObject (item) {
return (item && typeof item === "object" && !Array.isArray(item));
}
/**
* Deep merge two objects.
* @param target
* @param ...sources
*/
export function mergeDeep (target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
mergeDeep(target[key], source[key]);
}
else {
Object.assign(target, { [key]: source[key] });
}
}
}
return mergeDeep(target, ...sources);
}
/**
* Checks if object or array is empty
* @param {*} obj
* @returns
*/
export function isEmpty (obj) {
if (obj instanceof Array) {
return obj.length === 0;
}
else {
for (const prop in obj) {
if (Object.hasOwn(obj, prop)) {
return false;
}
}
return true;
}
}
export function addResourceLine (resourceConfig, field, attributesOverride, labelPrefix = null) {
const iconHref = resourceConfig.icon;
const elementType = resourceConfig.element;
const labelText = labelPrefix ? `${labelPrefix} ${resourceConfig.name}` : resourceConfig.name;
const id = resourceConfig.id;
const unitText = resourceConfig.unitText;
const attributes = { ...(resourceConfig.attributes), ...(attributesOverride) };
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
setSVGSrc(icon, iconHref);
setSVGAlt(icon, labelText);
field.append(icon);
const label = document.createElement("label");
label.innerText = labelText;
label.htmlFor = id;
field.append(label);
let element;
if (elementType === "input") {
element = document.createElement("input");
for (const k in attributes) {
element.setAttribute(k, attributes[k]);
}
element.id = id;
element.name = id;
element.required = true;
element.classList.add("w3-input");
element.classList.add("w3-border");
field.append(element);
}
else if (elementType === "select" || elementType === "multi-select") {
element = document.createElement("select");
for (const option of attributes.options) {
element.append(new Option(option));
}
element.value = attributes.value;
element.id = id;
element.name = id;
element.required = true;
element.classList.add("w3-select");
element.classList.add("w3-border");
if (elementType === "multi-select") {
element.setAttribute("multiple", true);
}
field.append(element);
}
else if (customElements.get(elementType)) {
element = document.createElement(elementType);
if (attributes.options) {
for (const option of attributes.options) {
element.append(new Option(option));
}
}
element.value = attributes.value;
element.id = id;
element.name = id;
element.required = true;
field.append(element);
}
let unit;
if (unitText) {
unit = document.createElement("p");
unit.innerText = unitText;
field.append(unit);
}
else {
unit = document.createElement("div");
unit.classList.add("hidden");
field.append(unit);
}
return { icon, label, element, unit };
} }

View File

@ -5,8 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title> <title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml"> <link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="modules/w3.css"> <link rel="stylesheet" href="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/style.css">
<link rel="stylesheet" href="css/nav.css"> <link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css"> <link rel="stylesheet" href="css/form.css">
@ -37,61 +36,50 @@
<h1>proxmox</h1> <h1>proxmox</h1>
<label for="navtoggle">&#9776;</label> <label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle"> <input type="checkbox" id="navtoggle">
<nav id="navigation"> <nav>
<a href="index.html">Instances</a> <a href="index.html">Instances</a>
<a href="account.html">Account</a> <a href="account.html">Account</a>
<a href="settings.html" aria-current="page">Settings</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> <a href="login.html">Logout</a>
</nav> </nav>
</header> </header>
<main> <main>
<h2>Settings</h2> <section>
<form id = "settings"> <h2>Settings</h2>
<section class="w3-card w3-padding"> <form id = "settings">
<h3>Synchronization Settings</h3> <div class="w3-card w3-padding">
<fieldset> <h3>Synchronization Settings</h3>
<legend>App Sync Method</legend> <fieldset>
<label><input class="w3-radio" type="radio" id="sync-always" name="sync-scheme" value="always" required>Always Sync</label> <legend>App Sync Method</legend>
<p>App will always periodically synchronize with Proxmox. High resource usage.</p> <label><input class="w3-radio" type="radio" id="sync-always" name="sync-scheme" value="always" required>Always Sync</label>
<label><input class="w3-radio" type="radio" id="sync-hash" name="sync-scheme" value="hash" required>Check For Sync</label> <p>App will always periodically synchronize with Proxmox. High 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-hash" name="sync-scheme" value="hash" required>Check For Sync</label>
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label> <p>App will periodically check for updates and synchronize only if needed. Medium resource usage.</p>
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p> <label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
</fieldset> <p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
<fieldset> </fieldset>
<legend>App Sync Frequency</legend> <fieldset>
<div class="input-grid" style="grid-template-columns: auto auto 1fr;"> <legend>App Sync Frequency</legend>
<p>Sync every</p><input aria-label="sync rate in seconds" class="w3-input w3-border" type="number" id="sync-rate" name="sync-rate" min="1" required><p>Second(s)</p> <div class="input-grid" style="grid-template-columns: auto auto 1fr;">
</div> <p>Sync every</p><input aria-label="sync rate in seconds" class="w3-input w3-border" type="number" id="sync-rate" name="sync-rate" min="1" required><p>Second(s)</p>
</fieldset> </div>
</section> </fieldset>
<section class="w3-card w3-padding"> </div>
<h3>Search Settings</h3> <div class="w3-card w3-padding">
<fieldset> <h3>Search Settings</h3>
<legend>Instance Search Criteria</legend> <fieldset>
<label><input class="w3-radio" type="radio" id="search-exact" name="search-criteria" value="exact" required>Exact Match</label> <legend>Instance Search Criteria</legend>
<p>Sorts by exact query match in instance name.</p> <label><input class="w3-radio" type="radio" id="search-exact" name="search-criteria" value="exact" required>Exact Match</label>
<label><input class="w3-radio" type="radio" id="search-fuzzy" name="search-criteria" value="fuzzy" required>Fuzzy Match</label> <p>Sorts by exact query match in instance name.</p>
<p>Sorts by best matching to worst matching.</p> <label><input class="w3-radio" type="radio" id="search-fuzzy" name="search-criteria" value="fuzzy" required>Fuzzy Match</label>
</fieldset> <p>Sorts by best matching to worst matching.</p>
</section> </fieldset>
<section class="w3-card w3-padding"> </div>
<h3>Appearance</h3> <div class="w3-container w3-center" id="form-actions">
<fieldset> <button class="w3-button w3-margin" id="save" type="submit">SAVE</button>
<legend>Default Theme</legend> </div>
<label>Theme<select class="w3-select w3-border" id="appearance-theme" name="appearance-theme" style="width: fit-content; padding-right: 24px;"> </form>
<option value="auto">Auto</option> </section>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</label>
</fieldset>
</section>
<div class="w3-container w3-center" id="form-actions">
<button class="w3-button w3-margin" id="save" type="submit">SAVE</button>
</div>
</form>
</main> </main>
</body> </body>
</html> </html>

102
user.html
View File

@ -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">&#9776;</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" style="margin-left: 0;">
<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>