Compare commits

8 Commits

15 changed files with 328 additions and 288 deletions

View File

@@ -6,81 +6,11 @@ import (
"net/http"
"proxmoxaas-dashboard/app/common"
"github.com/gerow/go-color"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
func HandleGETAccount(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
account, err := GetUserAccount(auth)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
for k, v := range account.Resources {
switch t := v.(type) {
case NumericResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
}
case StorageResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
}
case ListResource:
l := struct {
Type string
Display bool
Resources []ResourceChart
}{
Type: t.Type,
Display: t.Display,
Resources: []ResourceChart{},
}
for _, r := range t.Total {
l.Resources = append(l.Resources, ResourceChart{
Type: t.Type,
Display: t.Display,
Name: r.Name,
Used: r.Used,
Max: r.Max,
Avail: float64(r.Avail), // usually an int
Unit: "",
})
}
account.Resources[k] = l
}
}
c.HTML(http.StatusOK, "html/account.html", gin.H{
"global": common.Global,
"page": "account",
"account": account,
})
} else {
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
}
}
type Account struct {
Username string
Pools map[string]bool
@@ -143,14 +73,101 @@ type ListResource struct {
}
type ResourceChart struct {
Type string
Display bool
Name string
Used int64
Max int64
Avail float64
Prefix string
Unit string
Type string
Display bool
Name string
Used int64
Max int64
Avail float64
Prefix string
Unit string
ColorHex string
}
var Red = color.RGB{
R: 1,
G: 0,
B: 0,
}
var Green = color.RGB{
R: 0,
G: 1,
B: 0,
}
func HandleGETAccount(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
account, err := GetUserAccount(auth)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
for k, v := range account.Resources {
switch t := v.(type) {
case NumericResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(),
}
case StorageResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(),
}
case ListResource:
l := struct {
Type string
Display bool
Resources []ResourceChart
}{
Type: t.Type,
Display: t.Display,
Resources: []ResourceChart{},
}
for _, r := range t.Total {
l.Resources = append(l.Resources, ResourceChart{
Type: t.Type,
Display: t.Display,
Name: r.Name,
Used: r.Used,
Max: r.Max,
Avail: float64(r.Avail), // usually an int
Unit: "",
ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(),
})
}
account.Resources[k] = l
}
}
c.HTML(http.StatusOK, "html/account.html", gin.H{
"global": common.Global,
"page": "account",
"account": account,
})
} else {
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
}
}
func GetUserAccount(auth common.Auth) (Account, error) {
@@ -261,3 +278,15 @@ func FormatNumber(val int64, base int64) (float64, string) {
return 0, ""
}
}
// interpolate between min and max by normalized (0 - 1) val
func InterpolateColorHSV(min color.RGB, max color.RGB, val float64) color.RGB {
minhsl := min.ToHSL()
maxhsl := max.ToHSL()
interphsl := color.HSL{
H: (1-val)*minhsl.H + (val)*maxhsl.H,
S: (1-val)*minhsl.S + (val)*maxhsl.S,
L: (1-val)*minhsl.L + (val)*maxhsl.L,
}
return interphsl.ToRGB()
}

View File

@@ -13,6 +13,46 @@ import (
"github.com/go-viper/mapstructure/v2"
)
type VMPath struct {
Node string
Type string
VMID string
}
// imported types from fabric
type InstanceConfig struct {
Type fabric.InstanceType `json:"type"`
Name string `json:"name"`
Proctype string `json:"cpu"`
Cores uint64 `json:"cores"`
Memory uint64 `json:"memory"`
Swap uint64 `json:"swap"`
Volumes map[string]*fabric.Volume `json:"volumes"`
Nets map[string]*fabric.Net `json:"nets"`
Devices map[string]*fabric.Device `json:"devices"`
Boot fabric.BootOrder `json:"boot"`
// overrides
ProctypeSelect common.Select
}
type GlobalConfig struct {
CPU struct {
Whitelist bool
}
}
type UserConfig struct {
CPU struct {
Global []CPUConfig
Nodes map[string][]CPUConfig
}
}
type CPUConfig struct {
Name string
}
func HandleGETConfig(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
@@ -155,29 +195,6 @@ func ExtractVMPath(c *gin.Context) (VMPath, error) {
return vm_path, nil
}
type VMPath struct {
Node string
Type string
VMID string
}
// imported types from fabric
type InstanceConfig struct {
Type fabric.InstanceType `json:"type"`
Name string `json:"name"`
Proctype string `json:"cpu"`
Cores uint64 `json:"cores"`
Memory uint64 `json:"memory"`
Swap uint64 `json:"swap"`
Volumes map[string]*fabric.Volume `json:"volumes"`
Nets map[string]*fabric.Net `json:"nets"`
Devices map[string]*fabric.Device `json:"devices"`
Boot fabric.BootOrder `json:"boot"`
// overrides
ProctypeSelect common.Select
}
func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
config := InstanceConfig{}
path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID)
@@ -208,23 +225,6 @@ func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
return config, nil
}
type GlobalConfig struct {
CPU struct {
Whitelist bool
}
}
type UserConfig struct {
CPU struct {
Global []CPUConfig
Nodes map[string][]CPUConfig
}
}
type CPUConfig struct {
Name string
}
func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) {
cputypes := common.Select{
ID: "proctype",

View File

@@ -10,6 +10,39 @@ import (
"github.com/go-viper/mapstructure/v2"
)
// used in constructing instance cards in index
type Node struct {
Node string `json:"node"`
Status string `json:"status"`
}
// used in constructing instance cards in index
type InstanceCard struct {
VMID uint
Name string
Type string
Status string
Node string
NodeStatus string
ConfigPath string
ConsolePath string
}
// used in retriving cluster tasks
type Task struct {
Type string
Node string
User string
ID string
VMID uint
Status string
EndTime uint
}
type InstanceStatus struct {
Status string
}
func HandleGETIndex(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil { // user should be authed, try to return index with population
@@ -45,36 +78,6 @@ func HandleGETInstancesFragment(c *gin.Context) {
}
// used in constructing instance cards in index
type Node struct {
Node string `json:"node"`
Status string `json:"status"`
}
// used in constructing instance cards in index
type InstanceCard struct {
VMID uint
Name string
Type string
Status string
Node string
NodeStatus string
}
// used in retriving cluster tasks
type Task struct {
Type string
Node string
User string
ID string
VMID uint
Status string
}
type InstanceStatus struct {
Status string
}
func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) {
ctx := common.RequestContext{
Cookies: map[string]string{
@@ -116,6 +119,12 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
for vmid, instance := range instances {
nodestatus := nodes[instance.Node].Status
instance.NodeStatus = nodestatus
instance.ConfigPath = fmt.Sprintf("config?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
if instance.Type == "qemu" {
instance.ConsolePath = fmt.Sprintf("%s/?console=kvm&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&novnc=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
} else if instance.Type == "lxc" {
instance.ConsolePath = fmt.Sprintf("%s/?console=lxc&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&xtermjs=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
}
instances[vmid] = instance
}
@@ -128,6 +137,9 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
return nil, nil, fmt.Errorf("request to /cluster/tasks resulted in %+v", res)
}
most_recent_task := map[uint]uint{}
expected_state := map[uint]string{}
for _, v := range ctx.Body["data"].([]any) {
task := Task{}
err := mapstructure.Decode(v, &task)
@@ -151,8 +163,21 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
} else if !(task.Status == "running" || task.Status == "OK") { // task is not running or finished with status OK
continue
} else { // recent task is a start or stop task for user instance which is running or "OK"
if task.EndTime > most_recent_task[task.VMID] { // if the task's end time is later than the most recent one encountered
most_recent_task[task.VMID] = task.EndTime // update the most recent task
if task.Type == "qmstart" || task.Type == "vzstart" { // if the task was a start task, update the expected state to running
expected_state[task.VMID] = "running"
} else if task.Type == "qmstop" || task.Type == "vzstop" { // if the task was a stop task, update the expected state to stopped
expected_state[task.VMID] = "stopped"
}
}
}
}
for vmid, expected_state := range expected_state { // for the expected states from recent tasks
if instances[vmid].Status != expected_state { // if the current node's state from /cluster/resources differs from expected state
// get /status/current which is updated faster than /cluster/resources
instance := instances[task.VMID]
instance := instances[vmid]
path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID)
ctx.Body = map[string]any{}
res, code, err := common.RequestGetAPI(path, ctx)
@@ -167,7 +192,7 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
mapstructure.Decode(ctx.Body["data"], &status)
instance.Status = status.Status
instances[task.VMID] = instance
instances[vmid] = instance
}
}

View File

@@ -9,6 +9,18 @@ import (
"github.com/go-viper/mapstructure/v2"
)
// used when requesting GET /access/domains
type GetRealmsBody struct {
Data []Realm `json:"data"`
}
// stores each realm's data
type Realm struct {
Default int `json:"default"`
Realm string `json:"realm"`
Comment string `json:"comment"`
}
func GetLoginRealms() ([]Realm, error) {
realms := []Realm{}
@@ -37,18 +49,6 @@ func GetLoginRealms() ([]Realm, error) {
return realms, nil
}
// used when requesting GET /access/domains
type GetRealmsBody struct {
Data []Realm `json:"data"`
}
// stores each realm's data
type Realm struct {
Default int `json:"default"`
Realm string `json:"realm"`
Comment string `json:"comment"`
}
func HandleGETLogin(c *gin.Context) {
realms, err := GetLoginRealms()
if err != nil {

26
go.mod
View File

@@ -3,9 +3,11 @@ module proxmoxaas-dashboard
go 1.24
require (
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
github.com/gin-gonic/gin v1.10.0
github.com/go-viper/mapstructure/v2 v2.2.1
github.com/tdewolff/minify v2.3.6+incompatible
github.com/tdewolff/minify/v2 v2.23.5
proxmoxaas-fabric v0.0.0
)
@@ -16,17 +18,20 @@ require (
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/diskfs/go-diskfs v1.5.2 // indirect
github.com/diskfs/go-diskfs v1.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/elliotwutingfeng/asciiset v0.0.0-20240214025120-24af97c84155 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/luthermonson/go-proxmox v0.2.2 // indirect
@@ -35,17 +40,20 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/tdewolff/minify/v2 v2.23.1 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/xattr v0.4.10 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/parse/v2 v2.7.23 // indirect
github.com/tdewolff/parse/v2 v2.8.0 // indirect
github.com/tdewolff/test v1.0.11 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -26,7 +26,7 @@
<main>
<section>
<h2><a href="index">Instances</a> / {{.config.Name}}</h2>
<form>
<form id="config-form">
<fieldset class="w3-card w3-padding">
<legend>Resources</legend>
<div class="input-grid" id="resources" style="grid-template-columns: auto auto auto 1fr;">
@@ -91,7 +91,7 @@
</fieldset>
{{end}}
<div class="w3-container w3-center" id="form-actions">
<button class="w3-button w3-margin" id="exit" type="button">EXIT</button>
<button class="w3-button w3-margin" id="exit" type="submit">EXIT</button>
</div>
</form>
</section>

View File

@@ -35,7 +35,7 @@
<h2>Instances</h2>
<div class="w3-card w3-padding">
<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" tabindex="0">
<svg role="img" aria-label="Search Instances"><use href="images/common/search.svg#symb"></use></svg>
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
</form>

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -19,7 +19,7 @@ async function init () {
initNetworks();
initDevices();
document.querySelector("#exit").addEventListener("click", handleFormExit);
document.querySelector("#config-form").addEventListener("submit", handleFormExit);
}
class VolumeAction extends HTMLElement {
@@ -530,7 +530,8 @@ async function refreshBoot () {
}
}
async function handleFormExit () {
async function handleFormExit (event) {
event.preventDefault();
const body = {
cores: document.querySelector("#cores").value,
memory: document.querySelector("#ram").value

View File

@@ -1,4 +1,4 @@
import { requestPVE, requestAPI, goToPage, setAppearance, getSearchSettings, goToURL, requestDash, setSVGSrc, setSVGAlt } from "./utils.js";
import { requestPVE, requestAPI, setAppearance, getSearchSettings, requestDash, setSVGSrc, setSVGAlt } from "./utils.js";
import { alert, dialog } from "./dialog.js";
import { setupClientSync } from "./clientsync.js";
import wfaInit from "../modules/wfa.js";
@@ -122,33 +122,35 @@ class InstanceCard extends HTMLElement {
const powerButton = this.shadowRoot.querySelector("#power-btn");
if (powerButton.classList.contains("clickable")) {
powerButton.onclick = this.handlePowerButton.bind(this);
}
const configButton = this.shadowRoot.querySelector("#configure-btn");
if (configButton.classList.contains("clickable")) {
configButton.onclick = this.handleConfigButton.bind(this);
}
const consoleButton = this.shadowRoot.querySelector("#console-btn");
if (consoleButton.classList.contains("clickable")) {
consoleButton.classList.add("clickable");
consoleButton.onclick = this.handleConsoleButton.bind(this);
powerButton.onkeydown = (event) => {
console.log(event.key, event.key === "Enter")
if (event.key === "Enter") {
event.preventDefault()
this.handlePowerButton()
}
}
}
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
if (deleteButton.classList.contains("clickable")) {
deleteButton.onclick = this.handleDeleteButton.bind(this);
deleteButton.onkeydown = (event) => {
if (event.key === "Enter") {
event.preventDefault()
this.handleDeleteButton()
}
}
}
}
setStatusLoading() {
this.status = "loading"
let statusicon = this.shadowRoot.querySelector("#status")
let powerbtn = this.shadowRoot.querySelector("#power-btn")
setSVGSrc(statusicon, "images/status/loading.svg")
setSVGAlt(statusicon, "instance is loading")
setSVGSrc(powerbtn, "images/status/loading.svg")
setSVGAlt(powerbtn, "")
setStatusLoading () {
this.status = "loading";
const statusicon = this.shadowRoot.querySelector("#status");
const powerbtn = this.shadowRoot.querySelector("#power-btn");
setSVGSrc(statusicon, "images/status/loading.svg");
setSVGAlt(statusicon, "instance is loading");
setSVGSrc(powerbtn, "images/status/loading.svg");
setSVGAlt(powerbtn, "");
}
async handlePowerButton () {
@@ -161,7 +163,7 @@ class InstanceCard extends HTMLElement {
const targetAction = this.status === "running" ? "stop" : "start";
const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid });
this.setStatusLoading()
this.setStatusLoading();
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
@@ -186,20 +188,6 @@ class InstanceCard extends HTMLElement {
}
}
handleConfigButton () {
if (!this.actionLock && this.status === "stopped") { // if the action lock is false, and the node is stopped, then navigate to the config page with the node info in the search query
goToPage("config", { 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(window.PVE, data, true);
}
}
handleDeleteButton () {
if (!this.actionLock && this.status === "stopped") {
const header = `Delete VM ${this.vmid}`;

View File

@@ -114,20 +114,6 @@ export function goToPage (page, data = null) {
window.location.href = `${page}${data ? "?" : ""}${params}`;
}
export function goToURL (href, data = {}, newwindow = false) {
const url = new URL(href);
for (const k in data) {
url.searchParams.append(k, data[k]);
}
if (newwindow) {
window.open(url, document.title, "height=480,width=848");
}
else {
window.location.assign(url.toString());
}
}
export function getURIData () {
const url = new URL(window.location.href);
return Object.fromEntries(url.searchParams);

View File

@@ -237,51 +237,42 @@
{{end}}
{{define "boot"}}
<draggable-container id="enabled" data-group="boot">
<template shadowrootmode="open">
{{template "boot-style"}}
<label>Enabled</label>
<div id="wrapper" style="padding-bottom: 1em;">
{{range .Enabled}}
{{template "boot-target" .}}
{{end}}
</div>
</template>
</draggable-container>
{{template "boot-container" Map "ID" "enabled" "Name" "Enabled" "Targets" .Enabled}}
<hr style="padding: 0; margin: 0;">
<draggable-container id="disabled" data-group="boot">
<template shadowrootmode="open">
{{template "boot-style"}}
<label>Disabled</label>
<div id="wrapper" style="padding-bottom: 1em;">
{{range .Disabled}}
{{template "boot-target" .}}
{{end}}
</div>
</template>
</draggable-container>
{{template "boot-container" Map "ID" "disabled" "Name" "Disabled" "Targets" .Disabled}}
{{end}}
{{define "boot-style"}}
<style>
div.draggable-item.ghost {
border: 1px dashed var(--main-text-color);
border-radius: 5px;
margin: -1px;
}
div.draggable-item {
cursor: grab;
}
div.draggable-item svg {
height: 1em;
width: 1em;
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
</style>
{{define "boot-container"}}
<draggable-container id="{{.ID}}" data-group="boot">
<template shadowrootmode="open">
<style>
* {
box-sizing: border-box;
}
div.draggable-item.ghost {
border: 1px dashed var(--main-text-color);
border-radius: 5px;
margin: -1px;
}
div.draggable-item {
cursor: grab;
}
div.draggable-item svg {
height: 1em;
width: 1em;
}
#wrapper {
padding-bottom: 1em;
}
</style>
<label>{{.Name}}</label>
<div id="wrapper">
{{range .Targets}}
{{template "boot-target" .}}
{{end}}
</div>
</template>
</draggable-container>
{{end}}
{{define "boot-target"}}

View File

@@ -39,20 +39,24 @@
</div>
<div class="w3-col l2 m2 s6 flex row nowrap" style="height: 1lh;">
{{if and (eq .NodeStatus "online") (eq .Status "running")}}
<svg id="power-btn" class="clickable" aria-label="shutdown instance"><use href="images/actions/instance/stop.svg#symb"></svg>
<svg id="configure-btn" aria-label=""><use href="images/actions/instance/config-inactive.svg#symb"></svg>
<svg id="console-btn" class="clickable" aria-label="open console"><use href="images/actions/instance/console-active.svg#symb"></svg>
<svg id="delete-btn" aria-label=""><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
<svg id="power-btn" class="clickable" aria-label="shutdown instance" role="button" tabindex=0><use href="images/actions/instance/stop.svg#symb"></svg>
<svg id="configure-btn" aria-disabled="true" role="none"><use href="images/actions/instance/config-inactive.svg#symb"></svg>
<a href="{{.ConsolePath}}" target="_blank">
<svg id="console-btn" class="clickable" aria-label="open console"><use href="images/actions/instance/console-active.svg#symb"></svg>
</a>
<svg id="delete-btn" aria-disabled="true" role="none"><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
{{else if and (eq .NodeStatus "online") (eq .Status "stopped")}}
<svg id="power-btn" class="clickable" aria-label="start instance"><use href="images/actions/instance/start.svg#symb"></svg>
<svg id="configure-btn" class="clickable" aria-label="change configuration"><use href="images/actions/instance/config-active.svg#symb"></svg>
<svg id="console-btn" aria-label=""><use href="images/actions/instance/console-inactive.svg#symb"></svg>
<svg id="delete-btn" class="clickable" aria-label="delete instance"><use href="images/actions/instance/delete-active.svg#symb"></svg>
<svg id="power-btn" class="clickable" aria-label="start instance" role="button" tabindex=0><use href="images/actions/instance/start.svg#symb"></svg>
<a href="{{.ConfigPath}}">
<svg id="configure-btn" class="clickable" aria-label="change configuration"><use href="images/actions/instance/config-active.svg#symb"></svg>
</a>
<svg id="console-btn" aria-disabled="true" role="none"><use href="images/actions/instance/console-inactive.svg#symb"></svg>
<svg id="delete-btn" class="clickable" aria-label="delete instance" role="button" tabindex=0><use href="images/actions/instance/delete-active.svg#symb"></svg>
{{else if and (eq .NodeStatus "online") (eq .Status "loading")}}
<svg id="power-btn" aria-label=""><use href="images/actions/instance/loading.svg#symb"></svg>
<svg id="configure-btn" aria-label=""><use href="images/actions/instance/config-inactive.svg#symb"></svg>
<svg id="console-btn" aria-label=""><use href="images/actions/instance/console-inactive.svg#symb"></svg>
<svg id="delete-btn" aria-label=""><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
<svg id="power-btn" aria-disabled="true" role="none"><use href="images/actions/instance/loading.svg#symb"></svg>
<svg id="configure-btn" aria-disabled="true" role="none"><use href="images/actions/instance/config-inactive.svg#symb"></svg>
<svg id="console-btn" aria-disabled="true" role="none"><use href="images/actions/instance/console-inactive.svg#symb"></svg>
<svg id="delete-btn" aria-disabled="true" role="none"><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
{{else}}
{{end}}
</div>

View File

@@ -13,15 +13,13 @@
margin: 0;
width: 100%;
height: fit-content;
padding: 10px 10px 10px 10px;
padding: 10px;
border-radius: 5px;
}
progress {
width: 100%;
border: 0;
height: 1em;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
#caption {
@@ -30,14 +28,23 @@
display: flex;
flex-direction: column;
}
progress::-moz-progress-bar {
background: #{{.ColorHex}};
}
progress::-webkit-progress-bar {
background: var(--main-text-color);
}
progress::-webkit-progress-value {
background: #{{.ColorHex}};
}
</style>
<div id="container">
<progress value="{{.Used}}" max="{{.Max}}"></progress>
<p id="caption">
<progress value="{{.Used}}" max="{{.Max}}" id="resource"></progress>
<label id="caption" for="resource">
<span>{{.Name}}</span>
<span>{{printf "%g" .Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
</p>
</label>
</div>
</template>
</resource-chart>
{{end}}-
{{end}}