Compare commits
25 Commits
3f754c65b9
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f72aaf255 | |||
| 10ef24e76b | |||
| 00fa5f3152 | |||
| 94233000df | |||
| d88a208da5 | |||
| 3b1b20b506 | |||
| 66747fa657 | |||
| f40638598d | |||
| 0082f2f3e5 | |||
| 549316ab73 | |||
| fc58861046 | |||
| 9b7404c8d6 | |||
| 08cd4dfaaa | |||
| 26a21e6cc7 | |||
| c3fe936e05 | |||
| eb201de26b | |||
| 545061b2eb | |||
| 0692c1b96c | |||
| 4f9d19eb39 | |||
| c92c4c4e6d | |||
| f44df0446e | |||
| 7475ae30b7 | |||
| b2820a3051 | |||
| 7aadc03fc9 | |||
| 9606d62e78 |
+3
-3
@@ -1,3 +1,3 @@
|
||||
[submodule "ProxmoxAAS-Fabric"]
|
||||
path = ProxmoxAAS-Fabric
|
||||
url = https://git.tronnet.net/tronnet/ProxmoxAAS-Fabric
|
||||
[submodule "proxmoxaas-common-lib"]
|
||||
path = proxmoxaas-common-lib
|
||||
url = https://git.tronnet.net/tronnet/proxmoxaas-common-lib
|
||||
|
||||
@@ -4,7 +4,7 @@ build: clean
|
||||
@echo "======================== Building Binary ======================="
|
||||
# resolve symbolic links in web by copying it into dist/web/
|
||||
cp -rL web/ dist/web/
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -v -o dist/ .
|
||||
CGO_ENABLED=0 go build -tags release -ldflags="-s -w" -v -o dist/ .
|
||||
|
||||
test: clean
|
||||
go run .
|
||||
|
||||
Submodule ProxmoxAAS-Fabric deleted from b74696f566
+6
-2
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"proxmoxaas-dashboard/app/common"
|
||||
@@ -11,7 +12,10 @@ import (
|
||||
"github.com/tdewolff/minify/v2"
|
||||
)
|
||||
|
||||
func Run(configPath *string) {
|
||||
func Run() {
|
||||
configPath := flag.String("config", "config.json", "path to config.json file")
|
||||
flag.Parse()
|
||||
|
||||
common.Global = common.GetConfig(*configPath)
|
||||
|
||||
// setup static resources
|
||||
@@ -38,7 +42,7 @@ func Run(configPath *string) {
|
||||
router.GET("/settings", routes.HandleGETSettings)
|
||||
|
||||
// run on all interfaces with port
|
||||
log.Fatal(router.Run(fmt.Sprintf("0.0.0.0:%d", common.Global.Port)))
|
||||
log.Fatal("[Error] starting gin router: ", router.Run(fmt.Sprintf("0.0.0.0:%d", common.Global.Port)))
|
||||
}
|
||||
|
||||
// setup static resources under web (css, images, modules, scripts)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//go:build !release
|
||||
// +build !release
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/tdewolff/minify/v2"
|
||||
)
|
||||
|
||||
// defines mime type and associated minifier
|
||||
type MimeType struct {
|
||||
Type string
|
||||
Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error
|
||||
}
|
||||
|
||||
// debug mime types
|
||||
var MimeTypes = map[string]MimeType{
|
||||
"css": {
|
||||
Type: "text/css",
|
||||
Minifier: nil,
|
||||
},
|
||||
"html": {
|
||||
Type: "text/html",
|
||||
Minifier: nil,
|
||||
},
|
||||
"tmpl": {
|
||||
Type: "text/plain",
|
||||
Minifier: nil,
|
||||
},
|
||||
"frag": {
|
||||
Type: "text/plain",
|
||||
Minifier: nil,
|
||||
},
|
||||
"svg": {
|
||||
Type: "image/svg+xml",
|
||||
Minifier: nil,
|
||||
},
|
||||
"png": {
|
||||
Type: "image/png",
|
||||
Minifier: nil,
|
||||
},
|
||||
"js": {
|
||||
Type: "application/javascript",
|
||||
Minifier: nil,
|
||||
},
|
||||
"wasm": {
|
||||
Type: "application/wasm",
|
||||
Minifier: nil,
|
||||
},
|
||||
"*": {
|
||||
Type: "text/plain",
|
||||
Minifier: nil,
|
||||
},
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
//go:build release
|
||||
// +build release
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
@@ -55,45 +58,3 @@ var MimeTypes = map[string]MimeType{
|
||||
Minifier: nil,
|
||||
},
|
||||
}
|
||||
|
||||
// debug mime types
|
||||
/*
|
||||
var MimeTypes = map[string]MimeType{
|
||||
"css": {
|
||||
Type: "text/css",
|
||||
Minifier: nil,
|
||||
},
|
||||
"html": {
|
||||
Type: "text/html",
|
||||
Minifier: nil,
|
||||
},
|
||||
"tmpl": {
|
||||
Type: "text/plain",
|
||||
Minifier: nil,
|
||||
},
|
||||
"frag": {
|
||||
Type: "text/plain",
|
||||
Minifier: nil,
|
||||
},
|
||||
"svg": {
|
||||
Type: "image/svg+xml",
|
||||
Minifier: nil,
|
||||
},
|
||||
"png": {
|
||||
Type: "image/png",
|
||||
Minifier: nil,
|
||||
},
|
||||
"js": {
|
||||
Type: "application/javascript",
|
||||
Minifier: nil,
|
||||
},
|
||||
"wasm": {
|
||||
Type: "application/wasm",
|
||||
Minifier: nil,
|
||||
},
|
||||
"*": {
|
||||
Type: "text/plain",
|
||||
Minifier: nil,
|
||||
},
|
||||
}
|
||||
*/
|
||||
@@ -22,13 +22,6 @@ type StaticFile struct {
|
||||
MimeType MimeType
|
||||
}
|
||||
|
||||
// parsed vmpath data (ie node/type/vmid)
|
||||
type VMPath struct {
|
||||
Node string
|
||||
Type string
|
||||
VMID string
|
||||
}
|
||||
|
||||
// type used for templated <select>
|
||||
type Select struct {
|
||||
ID string
|
||||
@@ -43,16 +36,6 @@ type Option struct {
|
||||
Display string
|
||||
}
|
||||
|
||||
type RequestContext struct {
|
||||
Cookies map[string]string
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
Username string
|
||||
Token string
|
||||
CSRF string
|
||||
}
|
||||
|
||||
type Icon struct {
|
||||
ID string
|
||||
Src string
|
||||
|
||||
+68
-26
@@ -14,23 +14,34 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/tdewolff/minify/v2"
|
||||
|
||||
paas "proxmoxaas-common-lib"
|
||||
)
|
||||
|
||||
// get config file from configPath
|
||||
func GetConfig(configPath string) Config {
|
||||
content, err := os.ReadFile(configPath)
|
||||
root, err := os.OpenRoot(".")
|
||||
if err != nil {
|
||||
log.Fatal("Error when opening root dir: ", err)
|
||||
}
|
||||
defer root.Close()
|
||||
|
||||
content, err := root.ReadFile(configPath)
|
||||
if err != nil {
|
||||
log.Fatal("Error when opening config file: ", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
err = json.Unmarshal(content, &config)
|
||||
if err != nil {
|
||||
log.Fatal("Error during parsing config file: ", err)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -47,14 +58,14 @@ func InitMinify() *minify.M {
|
||||
|
||||
func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile {
|
||||
minified := make(map[string]StaticFile)
|
||||
fs.WalkDir(files, ".", func(path string, entry fs.DirEntry, err error) error {
|
||||
err := fs.WalkDir(files, ".", func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !entry.IsDir() {
|
||||
v, err := files.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Fatalf("error parsing template file %s: %s", path, err.Error())
|
||||
log.Fatalf("[Error] parsing template file %s: %s", path, err.Error())
|
||||
}
|
||||
x := strings.Split(entry.Name(), ".")
|
||||
if len(x) >= 2 { // file has extension
|
||||
@@ -62,7 +73,7 @@ func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile {
|
||||
if ok && mimetype.Minifier != nil { // if the extension is mapped in MimeTypes and has a minifier
|
||||
min, err := m.String(mimetype.Type, string(v)) // try to minify
|
||||
if err != nil {
|
||||
log.Fatalf("error minifying file %s: %s", path, err.Error())
|
||||
log.Fatalf("[Error] minifying file %s: %s", path, err.Error())
|
||||
}
|
||||
minified[path] = StaticFile{
|
||||
Data: min,
|
||||
@@ -84,7 +95,13 @@ func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return minified
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[Error] MinifyStatic: %s", err)
|
||||
return nil
|
||||
} else {
|
||||
return minified
|
||||
}
|
||||
}
|
||||
|
||||
func LoadHTMLToGin(engine *gin.Engine, html map[string]StaticFile) *template.Template {
|
||||
@@ -159,13 +176,16 @@ func HandleNonFatalError(c *gin.Context, err error) {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func RequestGetAPI(path string, context RequestContext, body any) (*http.Response, int, error) {
|
||||
func RequestGetAPI(path string, auth *paas.Auth, body any) (*http.Response, int, error) {
|
||||
req, err := http.NewRequest("GET", Global.API+path, nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
for k, v := range context.Cookies {
|
||||
req.AddCookie(&http.Cookie{Name: k, Value: v})
|
||||
|
||||
if auth != nil {
|
||||
for k, v := range GetRequestContextFromCookies(*auth) {
|
||||
req.AddCookie(&http.Cookie{Name: k, Value: v, Secure: true})
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
@@ -185,7 +205,6 @@ func RequestGetAPI(path string, context RequestContext, body any) (*http.Respons
|
||||
if err != nil {
|
||||
return nil, response.StatusCode, err
|
||||
}
|
||||
|
||||
switch body.(type) { // write json to body object depending on type, currently supports map[string]any (ie json) or []any (ie array of json)
|
||||
case *map[string]any:
|
||||
err = json.Unmarshal(data, &body)
|
||||
@@ -203,34 +222,41 @@ func RequestGetAPI(path string, context RequestContext, body any) (*http.Respons
|
||||
return response, response.StatusCode, nil
|
||||
}
|
||||
|
||||
func GetAuth(c *gin.Context) (Auth, error) {
|
||||
func GetAuthFromRequest(c *gin.Context) (paas.Auth, error) {
|
||||
_, errAuth := c.Cookie("auth")
|
||||
username, errUsername := c.Cookie("username")
|
||||
token, errToken := c.Cookie("PVEAuthCookie")
|
||||
csrf, errCSRF := c.Cookie("CSRFPreventionToken")
|
||||
if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil {
|
||||
return Auth{}, fmt.Errorf("error occured getting user cookies: (auth: %s, token: %s, csrf: %s)", errAuth, errToken, errCSRF)
|
||||
access, errAccess := c.Cookie("PAASAccessManagerTicket")
|
||||
if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil || errAccess != nil {
|
||||
return paas.Auth{}, fmt.Errorf("error occured getting user cookies: (auth: %s, token: %s, csrf: %s)", errAuth, errToken, errCSRF)
|
||||
} else {
|
||||
return Auth{username, token, csrf}, nil
|
||||
return paas.Auth{Username: username, Token: token, CSRF: csrf, AccessManagerTicket: access}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractVMPath(c *gin.Context) (VMPath, error) {
|
||||
func GetInstancePathFromRequest(c *gin.Context) (paas.InstancePath, error) {
|
||||
req_node := c.Query("node")
|
||||
req_type := c.Query("type")
|
||||
req_vmid := c.Query("vmid")
|
||||
if req_node == "" || req_type == "" || req_vmid == "" {
|
||||
return VMPath{}, fmt.Errorf("request missing required values: (node: %s, type: %s, vmid: %s)", req_node, req_type, req_vmid)
|
||||
return paas.InstancePath{}, fmt.Errorf("request missing required values: (node: %s, type: %s, vmid: %s)", req_node, req_type, req_vmid)
|
||||
}
|
||||
vm_path := VMPath{
|
||||
Node: req_node,
|
||||
Type: req_type,
|
||||
VMID: req_vmid,
|
||||
|
||||
vmid_int, err := strconv.ParseUint(req_vmid, 10, 64)
|
||||
if err != nil {
|
||||
return paas.InstancePath{}, err
|
||||
}
|
||||
|
||||
vm_path := paas.InstancePath{
|
||||
NodeName: req_node,
|
||||
InstanceType: paas.InstanceType(req_type),
|
||||
InstanceID: paas.InstanceID(vmid_int),
|
||||
}
|
||||
return vm_path, nil
|
||||
}
|
||||
|
||||
func FormatNumber(val int64, base int64) (float64, string) {
|
||||
func FormatNumber(val int64, base int64) (string, string) {
|
||||
valf := float64(val)
|
||||
basef := float64(base)
|
||||
steps := 0
|
||||
@@ -239,13 +265,29 @@ func FormatNumber(val int64, base int64) (float64, string) {
|
||||
steps++
|
||||
}
|
||||
|
||||
if base == 1000 {
|
||||
switch base {
|
||||
case 1000:
|
||||
s := fmt.Sprintf("%.4f", valf)
|
||||
s = strings.TrimRight(s, "0")
|
||||
s = strings.TrimRight(s, ".")
|
||||
prefixes := []string{"", "K", "M", "G", "T"}
|
||||
return valf, prefixes[steps]
|
||||
} else if base == 1024 {
|
||||
return s, prefixes[steps]
|
||||
case 1024:
|
||||
s := fmt.Sprintf("%.4f", valf)
|
||||
s = strings.TrimRight(s, "0")
|
||||
s = strings.TrimRight(s, ".")
|
||||
prefixes := []string{"", "Ki", "Mi", "Gi", "Ti"}
|
||||
return valf, prefixes[steps]
|
||||
} else {
|
||||
return 0, ""
|
||||
return s, prefixes[steps]
|
||||
default:
|
||||
return "0", ""
|
||||
}
|
||||
}
|
||||
|
||||
func GetRequestContextFromCookies(auth paas.Auth) map[string]string {
|
||||
return map[string]string{
|
||||
"username": auth.Username,
|
||||
"PVEAuthCookie": auth.Token,
|
||||
"CSRFPreventionToken": auth.CSRF,
|
||||
"PAASAccessManagerTicket": auth.AccessManagerTicket,
|
||||
}
|
||||
}
|
||||
|
||||
+138
-126
@@ -3,6 +3,7 @@ package routes
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
paas "proxmoxaas-common-lib"
|
||||
"proxmoxaas-dashboard/app/common"
|
||||
|
||||
"github.com/gerow/go-color"
|
||||
@@ -11,14 +12,8 @@ import (
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Username string
|
||||
Pools map[string]bool
|
||||
Nodes map[string]bool
|
||||
VMID struct {
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
Resources map[string]map[string]any
|
||||
paas.User
|
||||
Pools map[string]paas.Pool
|
||||
}
|
||||
|
||||
// numerical constraint
|
||||
@@ -82,7 +77,7 @@ type ResourceChart struct {
|
||||
Name string
|
||||
Used int64
|
||||
Max int64
|
||||
Avail float64
|
||||
Avail string
|
||||
Prefix string
|
||||
Unit string
|
||||
ColorHex string
|
||||
@@ -101,72 +96,86 @@ var Green = color.RGB{
|
||||
}
|
||||
|
||||
func HandleGETAccount(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil {
|
||||
account, err := GetUserAccount(auth)
|
||||
|
||||
account, err := GetUser(auth)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// for each resource category, create a resource chart
|
||||
for category, resources := range account.Resources {
|
||||
for resource, v := range resources {
|
||||
switch t := v.(type) {
|
||||
case NumericResource:
|
||||
avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
||||
account.Resources[category][resource] = 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 := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
||||
account.Resources[category][resource] = 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{},
|
||||
}
|
||||
pools, err := GetUserPools(auth)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range t.Total {
|
||||
l.Resources = append(l.Resources, ResourceChart{
|
||||
for poolname, pool := range pools {
|
||||
// for each resource category
|
||||
for category := range pool.Resources {
|
||||
// for each resource in each category
|
||||
for resource, v := range pool.Resources[category].(map[string]any) {
|
||||
// create a resource chart for resource depending on resource type
|
||||
switch t := v.(type) {
|
||||
case NumericResource:
|
||||
avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
||||
pools[poolname].Resources[category].(map[string]any)[resource] = 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(),
|
||||
})
|
||||
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 := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
||||
pools[poolname].Resources[category].(map[string]any)[resource] = 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 {
|
||||
avail := fmt.Sprintf("%d", r.Avail)
|
||||
l.Resources = append(l.Resources, ResourceChart{
|
||||
Type: t.Type,
|
||||
Display: t.Display,
|
||||
Name: r.Name,
|
||||
Used: r.Used,
|
||||
Max: r.Max,
|
||||
Avail: avail, // usually an int
|
||||
Unit: "",
|
||||
ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(),
|
||||
})
|
||||
}
|
||||
pools[poolname].Resources[category].(map[string]any)[resource] = l
|
||||
}
|
||||
account.Resources[category][resource] = l
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
account.Pools = pools
|
||||
|
||||
c.HTML(http.StatusOK, "html/account.html", gin.H{
|
||||
"global": common.Global,
|
||||
"page": "account",
|
||||
@@ -177,97 +186,100 @@ func HandleGETAccount(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserAccount(auth common.Auth) (Account, error) {
|
||||
account := Account{
|
||||
Resources: map[string]map[string]any{},
|
||||
}
|
||||
|
||||
ctx := common.RequestContext{
|
||||
Cookies: map[string]string{
|
||||
"username": auth.Username,
|
||||
"PVEAuthCookie": auth.Token,
|
||||
"CSRFPreventionToken": auth.CSRF,
|
||||
},
|
||||
}
|
||||
|
||||
// get user account basic data
|
||||
func GetUser(auth paas.Auth) (Account, error) {
|
||||
account := Account{}
|
||||
body := map[string]any{}
|
||||
res, code, err := common.RequestGetAPI("/user/config/cluster", ctx, &body)
|
||||
res, code, err := common.RequestGetAPI(fmt.Sprintf("/access/users/%s", auth.Username), &auth, &body)
|
||||
if err != nil {
|
||||
return account, err
|
||||
}
|
||||
if code != 200 {
|
||||
return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res)
|
||||
return account, fmt.Errorf("request to /access/pools resulted in %+v", res)
|
||||
}
|
||||
err = mapstructure.Decode(body, &account)
|
||||
if err != nil {
|
||||
return account, err
|
||||
} else {
|
||||
account.Username = auth.Username
|
||||
}
|
||||
return account, err
|
||||
}
|
||||
|
||||
body = map[string]any{}
|
||||
// get user resources
|
||||
res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx, &body)
|
||||
func GetUserPools(auth paas.Auth) (map[string]paas.Pool, error) {
|
||||
pools := map[string]paas.Pool{}
|
||||
|
||||
// get all pools
|
||||
body := map[string]any{}
|
||||
res, code, err := common.RequestGetAPI("/access/pools", &auth, &body)
|
||||
if err != nil {
|
||||
return account, err
|
||||
return pools, err
|
||||
}
|
||||
if code != 200 {
|
||||
return account, fmt.Errorf("request to /user/dynamic/resources resulted in %+v", res)
|
||||
return pools, fmt.Errorf("request to /access/pools resulted in %+v", res)
|
||||
}
|
||||
err = mapstructure.Decode(body["pools"].(map[string]any), &pools)
|
||||
if err != nil {
|
||||
return pools, err
|
||||
}
|
||||
resources := body
|
||||
|
||||
// get global config for resource type metadata
|
||||
body = map[string]any{}
|
||||
// get resource meta data
|
||||
res, code, err = common.RequestGetAPI("/global/config/resources", ctx, &body)
|
||||
res, code, err = common.RequestGetAPI("/global/config/resources", &auth, &body)
|
||||
if err != nil {
|
||||
return account, err
|
||||
return pools, err
|
||||
}
|
||||
if code != 200 {
|
||||
return account, fmt.Errorf("request to /global/config/resources resulted in %+v", res)
|
||||
return pools, fmt.Errorf("request to /global/config/resources resulted in %+v", res)
|
||||
}
|
||||
meta := body["resources"].(map[string]any)
|
||||
|
||||
// build each resource by its meta type
|
||||
for k, v := range meta {
|
||||
m := v.(map[string]any)
|
||||
t := m["type"].(string)
|
||||
r := resources[k].(map[string]any)
|
||||
category := m["category"].(string)
|
||||
if _, ok := account.Resources[category]; !ok {
|
||||
account.Resources[category] = map[string]any{}
|
||||
}
|
||||
if t == "numeric" {
|
||||
n := NumericResource{}
|
||||
n.Type = t
|
||||
err_m := mapstructure.Decode(m, &n)
|
||||
err_r := mapstructure.Decode(r, &n)
|
||||
if err_m != nil || err_r != nil {
|
||||
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||
// for each pool
|
||||
for poolname, pool := range pools {
|
||||
// for each resource in pool data
|
||||
for k, v := range pool.Resources {
|
||||
m := meta[k].(map[string]any)
|
||||
t := m["type"].(string)
|
||||
r := v.(map[string]any)
|
||||
category := m["category"].(string)
|
||||
|
||||
// create a category if it does not already exist
|
||||
if _, ok := pool.Resources[category]; !ok {
|
||||
pool.Resources[category] = map[string]any{}
|
||||
}
|
||||
account.Resources[category][k] = n
|
||||
} else if t == "storage" {
|
||||
n := StorageResource{}
|
||||
n.Type = t
|
||||
err_m := mapstructure.Decode(m, &n)
|
||||
err_r := mapstructure.Decode(r, &n)
|
||||
if err_m != nil || err_r != nil {
|
||||
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||
|
||||
// depending on type, decode the pool data into the corresponding resource type
|
||||
switch t {
|
||||
case "numeric":
|
||||
n := NumericResource{}
|
||||
n.Type = t
|
||||
err_m := mapstructure.Decode(m, &n)
|
||||
err_r := mapstructure.Decode(r, &n)
|
||||
if err_m != nil || err_r != nil {
|
||||
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||
}
|
||||
pools[poolname].Resources[category].(map[string]any)[k] = n
|
||||
case "storage":
|
||||
n := StorageResource{}
|
||||
n.Type = t
|
||||
err_m := mapstructure.Decode(m, &n)
|
||||
err_r := mapstructure.Decode(r, &n)
|
||||
if err_m != nil || err_r != nil {
|
||||
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||
}
|
||||
pools[poolname].Resources[category].(map[string]any)[k] = n
|
||||
case "list":
|
||||
n := ListResource{}
|
||||
n.Type = t
|
||||
err_m := mapstructure.Decode(m, &n)
|
||||
err_r := mapstructure.Decode(r, &n)
|
||||
if err_m != nil || err_r != nil {
|
||||
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||
}
|
||||
pools[poolname].Resources[category].(map[string]any)[k] = n
|
||||
}
|
||||
account.Resources[category][k] = n
|
||||
} else if t == "list" {
|
||||
n := ListResource{}
|
||||
n.Type = t
|
||||
err_m := mapstructure.Decode(m, &n)
|
||||
err_r := mapstructure.Decode(r, &n)
|
||||
if err_m != nil || err_r != nil {
|
||||
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||
}
|
||||
account.Resources[category][k] = n
|
||||
|
||||
// delete the old entry, only categories should be left at the end of the loop
|
||||
delete(pools[poolname].Resources, k)
|
||||
}
|
||||
}
|
||||
|
||||
return account, nil
|
||||
return pools, nil
|
||||
}
|
||||
|
||||
// interpolate between min and max by normalized (0 - 1) val
|
||||
|
||||
+15
-20
@@ -2,8 +2,8 @@ package routes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
paas "proxmoxaas-common-lib"
|
||||
"proxmoxaas-dashboard/app/common"
|
||||
"time"
|
||||
|
||||
@@ -21,9 +21,9 @@ type InstanceBackup struct {
|
||||
}
|
||||
|
||||
func HandleGETBackups(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil {
|
||||
vm_path, err := common.ExtractVMPath(c)
|
||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
@@ -39,8 +39,6 @@ func HandleGETBackups(c *gin.Context) {
|
||||
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error()))
|
||||
}
|
||||
|
||||
log.Printf("%+v", backups)
|
||||
|
||||
c.HTML(http.StatusOK, "html/backups.html", gin.H{
|
||||
"global": common.Global,
|
||||
"page": "backups",
|
||||
@@ -53,9 +51,9 @@ func HandleGETBackups(c *gin.Context) {
|
||||
}
|
||||
|
||||
func HandleGETBackupsFragment(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil { // user should be authed, try to return index with population
|
||||
vm_path, err := common.ExtractVMPath(c)
|
||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
@@ -67,27 +65,24 @@ func HandleGETBackupsFragment(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/plain")
|
||||
common.TMPL.ExecuteTemplate(c.Writer, "html/backups-backups.go.tmpl", gin.H{
|
||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/backups-backups.go.tmpl", gin.H{
|
||||
"backups": backups,
|
||||
})
|
||||
c.Status(http.StatusOK)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
} else { // return 401
|
||||
c.Status(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func GetInstanceBackups(vm common.VMPath, auth common.Auth) ([]InstanceBackup, error) {
|
||||
func GetInstanceBackups(vm paas.InstancePath, auth paas.Auth) ([]InstanceBackup, error) {
|
||||
backups := []InstanceBackup{}
|
||||
path := fmt.Sprintf("/cluster/%s/%s/%s/backup", vm.Node, vm.Type, vm.VMID)
|
||||
ctx := common.RequestContext{
|
||||
Cookies: map[string]string{
|
||||
"username": auth.Username,
|
||||
"PVEAuthCookie": auth.Token,
|
||||
"CSRFPreventionToken": auth.CSRF,
|
||||
},
|
||||
}
|
||||
path := fmt.Sprintf("/cluster/%s/%s/%d/backup", vm.NodeName, string(vm.InstanceType), uint64(vm.InstanceID))
|
||||
body := []any{}
|
||||
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||
res, code, err := common.RequestGetAPI(path, &auth, &body)
|
||||
if err != nil {
|
||||
return backups, err
|
||||
}
|
||||
@@ -102,7 +97,7 @@ func GetInstanceBackups(vm common.VMPath, auth common.Auth) ([]InstanceBackup, e
|
||||
|
||||
for i := range backups {
|
||||
size, prefix := common.FormatNumber(backups[i].Size, 1024)
|
||||
backups[i].SizeFormatted = fmt.Sprintf("%.3g %sB", size, prefix)
|
||||
backups[i].SizeFormatted = fmt.Sprintf("%s %sB", size, prefix)
|
||||
|
||||
t := time.Unix(backups[i].CTime, 0)
|
||||
backups[i].TimeFormatted = t.Format("02-01-06 15:04:05")
|
||||
|
||||
+62
-73
@@ -3,8 +3,8 @@ package routes
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
paas "proxmoxaas-common-lib"
|
||||
"proxmoxaas-dashboard/app/common"
|
||||
fabric "proxmoxaas-fabric/app"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
@@ -15,16 +15,7 @@ import (
|
||||
// imported types from fabric
|
||||
|
||||
type InstanceConfig struct {
|
||||
Type fabric.InstanceType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
CPU 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"`
|
||||
paas.Instance `mapstructure:",squash"`
|
||||
// overrides
|
||||
ProctypeSelect common.Select
|
||||
}
|
||||
@@ -35,21 +26,17 @@ type GlobalConfig struct {
|
||||
}
|
||||
}
|
||||
|
||||
type UserConfigResources struct {
|
||||
type PoolConfig struct {
|
||||
CPU struct {
|
||||
Global []CPUConfig
|
||||
Nodes map[string][]CPUConfig
|
||||
Global []paas.MatchLimit
|
||||
Nodes map[string][]paas.MatchLimit
|
||||
}
|
||||
}
|
||||
|
||||
type CPUConfig struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func HandleGETConfig(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil {
|
||||
vm_path, err := common.ExtractVMPath(c)
|
||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
@@ -61,13 +48,13 @@ func HandleGETConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
if config.Type == "VM" { // if VM, fetch CPU types from node
|
||||
config.ProctypeSelect, err = GetCPUTypes(vm_path, auth)
|
||||
config.ProctypeSelect, err = GetCPUTypes(vm_path, config.Pool, auth)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting proctypes: %s", err.Error()))
|
||||
}
|
||||
}
|
||||
for i, cpu := range config.ProctypeSelect.Options {
|
||||
if cpu.Value == config.CPU {
|
||||
if cpu.Value == config.Proctype {
|
||||
config.ProctypeSelect.Options[i].Selected = true
|
||||
}
|
||||
}
|
||||
@@ -83,9 +70,9 @@ func HandleGETConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
func HandleGETConfigVolumesFragment(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil {
|
||||
vm_path, err := common.ExtractVMPath(c)
|
||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
@@ -97,19 +84,23 @@ func HandleGETConfigVolumesFragment(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/plain")
|
||||
common.TMPL.ExecuteTemplate(c.Writer, "html/config-volumes.go.tmpl", gin.H{
|
||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-volumes.go.tmpl", gin.H{
|
||||
"config": config,
|
||||
})
|
||||
c.Status(http.StatusOK)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
} else {
|
||||
c.Status(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGETConfigNetsFragment(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil {
|
||||
vm_path, err := common.ExtractVMPath(c)
|
||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
@@ -121,19 +112,23 @@ func HandleGETConfigNetsFragment(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/plain")
|
||||
common.TMPL.ExecuteTemplate(c.Writer, "html/config-nets.go.tmpl", gin.H{
|
||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-nets.go.tmpl", gin.H{
|
||||
"config": config,
|
||||
})
|
||||
c.Status(http.StatusOK)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
} else {
|
||||
c.Status(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGETConfigDevicesFragment(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil {
|
||||
vm_path, err := common.ExtractVMPath(c)
|
||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
@@ -145,19 +140,23 @@ func HandleGETConfigDevicesFragment(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/plain")
|
||||
common.TMPL.ExecuteTemplate(c.Writer, "html/config-devices.go.tmpl", gin.H{
|
||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-devices.go.tmpl", gin.H{
|
||||
"config": config,
|
||||
})
|
||||
c.Status(http.StatusOK)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
} else {
|
||||
c.Status(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGETConfigBootFragment(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil {
|
||||
vm_path, err := common.ExtractVMPath(c)
|
||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
||||
if err != nil {
|
||||
common.HandleNonFatalError(c, err)
|
||||
return
|
||||
@@ -169,27 +168,24 @@ func HandleGETConfigBootFragment(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "text/plain")
|
||||
common.TMPL.ExecuteTemplate(c.Writer, "html/config-boot.go.tmpl", gin.H{
|
||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-boot.go.tmpl", gin.H{
|
||||
"config": config,
|
||||
})
|
||||
c.Status(http.StatusOK)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
} else {
|
||||
c.Status(http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
func GetInstanceConfig(vm common.VMPath, auth common.Auth) (InstanceConfig, error) {
|
||||
func GetInstanceConfig(vm paas.InstancePath, auth paas.Auth) (InstanceConfig, error) {
|
||||
config := InstanceConfig{}
|
||||
path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID)
|
||||
ctx := common.RequestContext{
|
||||
Cookies: map[string]string{
|
||||
"username": auth.Username,
|
||||
"PVEAuthCookie": auth.Token,
|
||||
"CSRFPreventionToken": auth.CSRF,
|
||||
},
|
||||
}
|
||||
path := fmt.Sprintf("/cluster/%s/%s/%d/", vm.NodeName, string(vm.InstanceType), uint64(vm.InstanceID))
|
||||
body := map[string]any{}
|
||||
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||
res, code, err := common.RequestGetAPI(path, &auth, &body)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
@@ -208,60 +204,53 @@ func GetInstanceConfig(vm common.VMPath, auth common.Auth) (InstanceConfig, erro
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) {
|
||||
func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Select, error) {
|
||||
cputypes := common.Select{
|
||||
ID: "proctype",
|
||||
Required: true,
|
||||
}
|
||||
|
||||
// get global resource config
|
||||
ctx := common.RequestContext{
|
||||
Cookies: map[string]string{
|
||||
"username": auth.Username,
|
||||
"PVEAuthCookie": auth.Token,
|
||||
"CSRFPreventionToken": auth.CSRF,
|
||||
},
|
||||
}
|
||||
body := map[string]any{}
|
||||
path := "/global/config/resources"
|
||||
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||
res, code, err := common.RequestGetAPI(path, &auth, &body)
|
||||
if err != nil {
|
||||
return cputypes, err
|
||||
}
|
||||
if code != 200 {
|
||||
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||
}
|
||||
global := GlobalConfig{}
|
||||
err = mapstructure.Decode(body["resources"], &global)
|
||||
globalConfig := GlobalConfig{}
|
||||
err = mapstructure.Decode(body["resources"], &globalConfig)
|
||||
if err != nil {
|
||||
return cputypes, err
|
||||
}
|
||||
|
||||
// get user resource config
|
||||
// get pool resource config
|
||||
body = map[string]any{}
|
||||
path = "/user/config/resources"
|
||||
res, code, err = common.RequestGetAPI(path, ctx, &body)
|
||||
path = fmt.Sprintf("/access/pools/%s", pool)
|
||||
res, code, err = common.RequestGetAPI(path, &auth, &body)
|
||||
if err != nil {
|
||||
return cputypes, err
|
||||
}
|
||||
if code != 200 {
|
||||
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||
}
|
||||
user := UserConfigResources{}
|
||||
err = mapstructure.Decode(body, &user)
|
||||
poolCPUConfig := PoolConfig{}
|
||||
err = mapstructure.Decode(body["pool"].(map[string]any)["resources"], &poolCPUConfig)
|
||||
if err != nil {
|
||||
return cputypes, err
|
||||
}
|
||||
|
||||
// use node specific rules if present, otherwise use global rules
|
||||
var userCPU []CPUConfig
|
||||
if _, ok := user.CPU.Nodes[vm.Node]; ok {
|
||||
userCPU = user.CPU.Nodes[vm.Node]
|
||||
var userCPU []paas.MatchLimit
|
||||
if _, ok := poolCPUConfig.CPU.Nodes[vm.NodeName]; ok {
|
||||
userCPU = poolCPUConfig.CPU.Nodes[vm.NodeName]
|
||||
} else {
|
||||
userCPU = user.CPU.Global
|
||||
userCPU = poolCPUConfig.CPU.Global
|
||||
}
|
||||
|
||||
if global.CPU.Whitelist { // cpu is a whitelist
|
||||
if globalConfig.CPU.Whitelist { // cpu is a whitelist
|
||||
for _, cpu := range userCPU { // for each cpu type in user config add it to the options
|
||||
cputypes.Options = append(cputypes.Options, common.Option{
|
||||
Display: cpu.Name,
|
||||
@@ -271,8 +260,8 @@ func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) {
|
||||
} else { // cpu is a blacklist
|
||||
// get the supported cpu types from the node
|
||||
body = map[string]any{}
|
||||
path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.Node)
|
||||
res, code, err = common.RequestGetAPI(path, ctx, &body)
|
||||
path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.NodeName)
|
||||
res, code, err = common.RequestGetAPI(path, &auth, &body)
|
||||
if err != nil {
|
||||
return cputypes, err
|
||||
}
|
||||
@@ -280,7 +269,7 @@ func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) {
|
||||
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||
}
|
||||
supported := struct {
|
||||
data []CPUConfig
|
||||
data []paas.MatchLimit
|
||||
}{}
|
||||
err = mapstructure.Decode(body, supported)
|
||||
if err != nil {
|
||||
@@ -289,7 +278,7 @@ func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) {
|
||||
|
||||
// for each node supported cpu type, if it is NOT in the user's config (aka is not blacklisted) then add it to the options
|
||||
for _, cpu := range supported.data {
|
||||
contains := slices.ContainsFunc(userCPU, func(c CPUConfig) bool {
|
||||
contains := slices.ContainsFunc(userCPU, func(c paas.MatchLimit) bool {
|
||||
return c.Name == cpu.Name
|
||||
})
|
||||
if !contains {
|
||||
|
||||
+37
-31
@@ -3,6 +3,7 @@ package routes
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
paas "proxmoxaas-common-lib"
|
||||
"proxmoxaas-dashboard/app/common"
|
||||
"strconv"
|
||||
|
||||
@@ -45,7 +46,7 @@ type InstanceStatus struct {
|
||||
}
|
||||
|
||||
func HandleGETIndex(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil { // user should be authed, try to return index with population
|
||||
instances, _, err := GetClusterResources(auth)
|
||||
if err != nil {
|
||||
@@ -64,7 +65,7 @@ func HandleGETIndex(c *gin.Context) {
|
||||
}
|
||||
|
||||
func HandleGETInstancesFragment(c *gin.Context) {
|
||||
auth, err := common.GetAuth(c)
|
||||
auth, err := common.GetAuthFromRequest(c)
|
||||
if err == nil { // user should be authed, try to return index with population
|
||||
instances, _, err := GetClusterResources(auth)
|
||||
if err != nil {
|
||||
@@ -72,25 +73,23 @@ func HandleGETInstancesFragment(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/plain")
|
||||
common.TMPL.ExecuteTemplate(c.Writer, "html/index-instances.go.tmpl", gin.H{
|
||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/index-instances.go.tmpl", gin.H{
|
||||
"instances": instances,
|
||||
})
|
||||
c.Status(http.StatusOK)
|
||||
if err != nil {
|
||||
c.Status(http.StatusInternalServerError)
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
} else { // return 401
|
||||
c.Status(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) {
|
||||
ctx := common.RequestContext{
|
||||
Cookies: map[string]string{
|
||||
"PVEAuthCookie": auth.Token,
|
||||
"CSRFPreventionToken": auth.CSRF,
|
||||
},
|
||||
}
|
||||
body := map[string]any{}
|
||||
res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", ctx, &body)
|
||||
func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node, error) {
|
||||
body := []any{}
|
||||
res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", &auth, &body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -102,16 +101,17 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
|
||||
nodes := map[string]Node{}
|
||||
|
||||
// parse /proxmox/cluster/resources to separate instances and nodes
|
||||
for _, v := range body["data"].([]any) {
|
||||
for _, v := range body {
|
||||
m := v.(map[string]any)
|
||||
if m["type"] == "node" { // if type is node -> parse as Node object
|
||||
switch m["type"] {
|
||||
case "node": // if type is node -> parse as Node object
|
||||
node := Node{}
|
||||
err := mapstructure.Decode(v, &node)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
nodes[node.Node] = node
|
||||
} else if m["type"] == "lxc" || m["type"] == "qemu" { // if type is lxc or qemu -> parse as InstanceCard object
|
||||
case "lxc", "qemu": // if type is lxc or qemu -> parse as InstanceCard object
|
||||
instance := InstanceCard{}
|
||||
err := mapstructure.Decode(v, &instance)
|
||||
if err != nil {
|
||||
@@ -127,9 +127,10 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
|
||||
// set instance's config link path
|
||||
instance.ConfigPath = fmt.Sprintf("config?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
|
||||
// set the instance's console link path
|
||||
if instance.Type == "qemu" {
|
||||
switch instance.Type {
|
||||
case "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" {
|
||||
case "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)
|
||||
}
|
||||
// set the instance's backups link path
|
||||
@@ -138,8 +139,8 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
|
||||
instances[vmid] = instance
|
||||
}
|
||||
|
||||
body = map[string]any{}
|
||||
res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", ctx, &body)
|
||||
body = []any{}
|
||||
res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", &auth, &body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -148,10 +149,10 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
|
||||
}
|
||||
|
||||
most_recent_task := map[uint]uint{}
|
||||
expected_state := map[uint]string{}
|
||||
expected_states := map[uint]string{}
|
||||
|
||||
// iterate through recent user accessible tasks to find the task most recently made on an instance
|
||||
for _, v := range body["data"].([]any) {
|
||||
for _, v := range body {
|
||||
// parse task as Task object
|
||||
task := Task{}
|
||||
err := mapstructure.Decode(v, &task)
|
||||
@@ -179,24 +180,25 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
|
||||
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"
|
||||
most_recent_task[task.VMID] = task.EndTime // update the most recent task
|
||||
switch task.Type {
|
||||
case "qmstart", "vzstart": // if the task was a start task, update the expected state to running
|
||||
expected_states[task.VMID] = "running"
|
||||
case "qmstop", "vzstop": // if the task was a stop task, update the expected state to stopped
|
||||
expected_states[task.VMID] = "stopped"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iterate through the instances with recent tasks, refetch their state from a more reliable source
|
||||
for vmid, expected_state := range expected_state { // for the expected states from recent tasks
|
||||
for vmid, expected_state := range expected_states { // 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[vmid]
|
||||
path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID)
|
||||
body = map[string]any{}
|
||||
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||
body := map[string]any{}
|
||||
res, code, err := common.RequestGetAPI(path, &auth, &body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -204,8 +206,12 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
|
||||
return nil, nil, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||
}
|
||||
|
||||
// attempt to decode task status as instance status
|
||||
status := InstanceStatus{}
|
||||
mapstructure.Decode(body["data"], &status)
|
||||
err = mapstructure.Decode(body, &status)
|
||||
if err != nil { // did not successfully decode task status, just skip
|
||||
continue
|
||||
}
|
||||
|
||||
instance.Status = status.Status
|
||||
instances[vmid] = instance
|
||||
|
||||
+3
-6
@@ -24,11 +24,8 @@ type Realm struct {
|
||||
func GetLoginRealms() ([]Realm, error) {
|
||||
realms := []Realm{}
|
||||
|
||||
ctx := common.RequestContext{
|
||||
Cookies: nil,
|
||||
}
|
||||
body := map[string]any{}
|
||||
res, code, err := common.RequestGetAPI("/proxmox/access/domains", ctx, &body)
|
||||
body := []any{}
|
||||
res, code, err := common.RequestGetAPI("/proxmox/access/domains", nil, &body)
|
||||
if err != nil {
|
||||
return realms, err
|
||||
}
|
||||
@@ -36,7 +33,7 @@ func GetLoginRealms() ([]Realm, error) {
|
||||
return realms, fmt.Errorf("request to /proxmox/access/domains resulted in %+v", res)
|
||||
}
|
||||
|
||||
for _, v := range body["data"].([]any) {
|
||||
for _, v := range body {
|
||||
v = v.(map[string]any)
|
||||
realm := Realm{}
|
||||
err := mapstructure.Decode(v, &realm)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func HandleGETSettings(c *gin.Context) {
|
||||
_, err := common.GetAuth(c)
|
||||
_, err := common.GetAuthFromRequest(c)
|
||||
if err == nil {
|
||||
c.HTML(http.StatusOK, "html/settings.html", gin.H{
|
||||
"global": common.Global,
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": "standard",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-tabs": [
|
||||
"error",
|
||||
{
|
||||
"allowIndentationTabs": true
|
||||
}
|
||||
],
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"brace-style": [
|
||||
"error",
|
||||
"stroustrup",
|
||||
{
|
||||
"allowSingleLine": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { defineConfig } from "eslint/config";
|
||||
import globals from "globals";
|
||||
import js from "@eslint/js";
|
||||
|
||||
export default defineConfig([js.configs.recommended,{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
rules: {
|
||||
"no-tabs": ["error", {
|
||||
allowIndentationTabs: true,
|
||||
}],
|
||||
indent: ["error", "tab"],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
quotes: ["error", "double"],
|
||||
semi: ["error", "always"],
|
||||
"brace-style": ["error", "stroustrup", { allowSingleLine: false }],
|
||||
"no-unused-vars": ["warn", {
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}],
|
||||
"prefer-const": ["error"]
|
||||
},
|
||||
}]);
|
||||
@@ -1,53 +1,46 @@
|
||||
module proxmoxaas-dashboard
|
||||
|
||||
go 1.26.0
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
github.com/tdewolff/minify/v2 v2.24.11
|
||||
proxmoxaas-fabric v0.0.0
|
||||
github.com/tdewolff/minify/v2 v2.24.13
|
||||
proxmoxaas-common-lib v0.0.0
|
||||
)
|
||||
|
||||
replace proxmoxaas-fabric => ./ProxmoxAAS-Fabric
|
||||
replace proxmoxaas-common-lib => ./proxmoxaas-common-lib
|
||||
|
||||
require (
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic v1.15.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/diskfs/go-diskfs v1.9.1 // indirect
|
||||
github.com/djherbis/times v1.6.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // 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.30.2 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.3 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // 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/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/luthermonson/go-proxmox v0.4.1 // indirect
|
||||
github.com/magefile/mage v1.17.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
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.3.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.1 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.11 // indirect
|
||||
github.com/quic-go/quic-go v0.59.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.13 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
|
||||
golang.org/x/arch v0.27.0 // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/net v0.55.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
+7
-8
@@ -4,17 +4,16 @@
|
||||
"description": "Front-end for ProxmoxAAS",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"lint": "html-validate --config configs/.htmlvalidate.json web/html/*; stylelint --config configs/.stylelintrc.json --formatter verbose --fix web/css/*.css; DEBUG=eslint:cli-engine eslint --config configs/.eslintrc.json --fix web/scripts/",
|
||||
"lint": "html-validate --config dev_config/.htmlvalidate.json web/html/*; stylelint --config dev_config/.stylelintrc.json --formatter verbose --fix web/css/*.css; DEBUG=eslint:cli-engine eslint --config dev_config/eslint.config.mjs --fix web/scripts/",
|
||||
"update-modules": "rm -rf web/modules/wfa.js web/modules/wfa.wasm; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.js -o web/modules/wfa.js; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.wasm -o web/modules/wfa.wasm"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^16.0.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"eslint": "^10.2.1",
|
||||
"globals": "^17.5.0",
|
||||
"html-validate": "^9.4.0",
|
||||
"stylelint": "^15.9.0",
|
||||
"stylelint-config-standard": "^33.0.0",
|
||||
"html-validate": "^9.4.0"
|
||||
"stylelint-config-standard": "^33.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Submodule
+1
Submodule proxmoxaas-common-lib added at ad6b8211b3
@@ -1,12 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
app "proxmoxaas-dashboard/app"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.json", "path to config.json file")
|
||||
flag.Parse()
|
||||
app.Run(configPath)
|
||||
app.Run()
|
||||
}
|
||||
|
||||
+25
-10
@@ -5,7 +5,7 @@ input, select, textarea {
|
||||
|
||||
.input-grid {
|
||||
display: grid;
|
||||
gap: 5px 10px;
|
||||
gap: 0.5em 1em;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -13,12 +13,12 @@ input, select, textarea {
|
||||
.input-grid * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.input-grid input {
|
||||
padding: 8px;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.input-grid img {
|
||||
@@ -34,17 +34,17 @@ legend {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.5em;
|
||||
margin-top: 0.25lh;
|
||||
margin-bottom: 0.25lh;
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
fieldset > *:last-child {
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
fieldset > .input-grid {
|
||||
@@ -66,7 +66,7 @@ input[type="radio"] {
|
||||
}
|
||||
|
||||
.w3-select, select {
|
||||
padding: 8px;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.w3-check {
|
||||
@@ -77,10 +77,25 @@ input[type="radio"] {
|
||||
:not(.input-grid) .input-grid + * {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
dialog {
|
||||
margin: auto;
|
||||
max-width: calc(min(100% - 16px, 80ch));
|
||||
color: var(--main-text-color);
|
||||
}
|
||||
|
||||
dialog #prompt {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
dialog button[value="confirm"] {
|
||||
background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);
|
||||
}
|
||||
|
||||
dialog button[value="cancel"] {
|
||||
background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);
|
||||
}
|
||||
+6
-6
@@ -53,7 +53,7 @@ header {
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 18px;
|
||||
font-size: var(--small-font-size);
|
||||
margin: 0;
|
||||
background-color: var(--nav-header-bg-color);
|
||||
color: var(--nav-header-text-color);
|
||||
@@ -61,15 +61,15 @@ header h1 {
|
||||
}
|
||||
|
||||
nav {
|
||||
font-size: var(--small-font-size);
|
||||
overflow: hidden;
|
||||
font-size: larger;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
nav a, header h1, label[for="navtoggle"] {
|
||||
text-align: left;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
height: 2em;
|
||||
@@ -80,7 +80,7 @@ label[for="navtoggle"], #navtoggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (width >= 600px){
|
||||
@media screen and (width >= 601px){
|
||||
header {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
@@ -106,7 +106,7 @@ label[for="navtoggle"], #navtoggle {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (width <= 600px){
|
||||
@media screen and (width <= 601px){
|
||||
header {
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
+30
-1
@@ -3,6 +3,9 @@
|
||||
--positive-color: #0f0;
|
||||
--highlight-color: yellow;
|
||||
--lightbg-text-color: black;
|
||||
--large-font-size: 32px;
|
||||
--medium-font-size: 24px;
|
||||
--small-font-size: 16px;
|
||||
}
|
||||
|
||||
@media screen and (prefers-color-scheme: dark) {
|
||||
@@ -41,9 +44,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
*, h1, h2, h3, h4, h5, h6, p {
|
||||
box-sizing: border-box;
|
||||
font-family: monospace;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--large-font-size);
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--medium-font-size);
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
h4, legend {
|
||||
font-size: var(--small-font-size);
|
||||
text-decoration: underline;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: var(--small-font-size);
|
||||
}
|
||||
|
||||
html {
|
||||
|
||||
+10
-43
@@ -34,58 +34,25 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{template "header" .}}
|
||||
</header>
|
||||
{{template "header" .}}
|
||||
<main>
|
||||
<h2>Account</h2>
|
||||
<section class="w3-card w3-padding">
|
||||
<h3>Account Details</h3>
|
||||
<p id="username">Username: {{.account.Username}}</p>
|
||||
<p id="pool">Pools: {{MapKeys .account.Pools ", "}}</p>
|
||||
<p id="vmid">VMID Range: {{.account.VMID.Min}} - {{.account.VMID.Max}}</p>
|
||||
<p id="nodes">Nodes: {{MapKeys .account.Nodes ", "}}</p>
|
||||
</section>
|
||||
<section class="w3-card w3-padding">
|
||||
<div class="flex row nowrap">
|
||||
<h3>Password</h3>
|
||||
<button class="w3-button w3-margin" id="change-password" type="button">Change Password</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="w3-card w3-padding">
|
||||
<h3>Cluster Resources</h3>
|
||||
<div>
|
||||
{{range $category, $v := .account.Resources}}
|
||||
{{if ne $category ""}}
|
||||
<h4>{{$category}}</h4>
|
||||
{{end}}
|
||||
<div class="resource-container">
|
||||
{{range $v}}
|
||||
{{if .Display}}
|
||||
{{if eq .Type "numeric"}}
|
||||
{{template "resource-chart" .}}
|
||||
{{end}}
|
||||
{{if eq .Type "storage"}}
|
||||
{{template "resource-chart" .}}
|
||||
{{end}}
|
||||
{{if eq .Type "list"}}
|
||||
{{range .Resources}}
|
||||
{{template "resource-chart" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<p id="username">Username: {{.account.Username.UserID}}@{{.account.Username.Realm}}</p>
|
||||
<p id="email">Email: {{.account.Mail}}</p>
|
||||
<p>Password: <button class="w3-button" id="change-password" type="button" style="padding: 0em; height: 1.5em; line-height: 1.5em;">Change Password</button></p>
|
||||
</section>
|
||||
{{range $poolname, $pool := .account.Pools}}
|
||||
{{template "pool-resources" $pool}}
|
||||
{{end}}
|
||||
</main>
|
||||
<template id="change-password-dialog">
|
||||
<link rel="stylesheet" href="modules/w3.css">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Change Password
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -97,8 +64,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{template "header" .}}
|
||||
</header>
|
||||
{{template "header" .}}
|
||||
<main>
|
||||
<h2><a href="index">Instances</a> / {{.config.Name}} / Backups</h2>
|
||||
<section class="w3-card w3-padding">
|
||||
|
||||
@@ -19,12 +19,10 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{template "header" .}}
|
||||
</header>
|
||||
{{template "header" .}}
|
||||
<main>
|
||||
<section>
|
||||
<h2><a href="index">Instances</a> / {{.config.Name}} / Config</h2>
|
||||
<h2><a href="index">Instances</a>/{{.config.Name}}/Config</h2>
|
||||
<form id="config-form">
|
||||
<fieldset class="w3-card w3-padding">
|
||||
<legend>Resources</legend>
|
||||
|
||||
+7
-9
@@ -65,16 +65,14 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{template "header" .}}
|
||||
</header>
|
||||
{{template "header" .}}
|
||||
<main>
|
||||
<section>
|
||||
<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" tabindex="0">
|
||||
<img alt="Search Instances" aria-label="Search Instances" src="images/common/search.svg#symb">
|
||||
<button type="submit" id="submit" class="w3-button" style="padding: 0; width: 1em; height: 1em; line-height: 1em;"><img alt="Search Instances" aria-label="Search Instances" src="images/common/search.svg#symb"></button>
|
||||
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
|
||||
</form>
|
||||
<!--Add Instance Button & Dialog Template-->
|
||||
@@ -84,7 +82,7 @@
|
||||
</button>
|
||||
<template id="create-instance-dialog">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Create New Instance
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -94,14 +92,14 @@
|
||||
<option value="lxc">Container</option>
|
||||
<option value="qemu">Virtual Machine</option>
|
||||
</select>
|
||||
<label for="pool">Pool</label>
|
||||
<select class="w3-select w3-border" name="pool" id="pool" required></select>
|
||||
<label for="node">Node</label>
|
||||
<select class="w3-select w3-border" name="node" id="node" required></select>
|
||||
<label for="name">Name</label>
|
||||
<input class="w3-input w3-border" name="name" id="name" type="text" required>
|
||||
<label for="vmid">ID</label>
|
||||
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required>
|
||||
<label for="pool">Pool</label>
|
||||
<select class="w3-select w3-border" name="pool" id="pool" required></select>
|
||||
<label for="cores">Cores (Threads)</label>
|
||||
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required>
|
||||
<label for="memory">Memory (MiB)</label>
|
||||
@@ -122,8 +120,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
+1
-3
@@ -7,9 +7,7 @@
|
||||
<link rel="modulepreload" href="scripts/dialog.js">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{template "header" .}}
|
||||
</header>
|
||||
{{template "header" .}}
|
||||
<main class="flex" style="justify-content: center; align-items: center;">
|
||||
<div class="w3-container w3-card w3-margin w3-padding" style="height: fit-content;">
|
||||
<h2 class="w3-center">{{.global.Organization}} Login</h2>
|
||||
|
||||
+7
-10
@@ -5,30 +5,25 @@
|
||||
<script src="scripts/settings.js" type="module"></script>
|
||||
<link rel="modulepreload" href="scripts/utils.js">
|
||||
<style>
|
||||
legend {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
column-gap: 10px;
|
||||
column-gap: 0.5em;
|
||||
}
|
||||
label + p {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 25px;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
p:last-child {
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
{{template "header" .}}
|
||||
</header>
|
||||
{{template "header" .}}
|
||||
<main>
|
||||
<h2>Settings</h2>
|
||||
<form id="settings">
|
||||
@@ -42,6 +37,8 @@
|
||||
<p>App will periodically check for updates and synchronize only if needed. Medium resource usage.</p>
|
||||
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
|
||||
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
|
||||
<label><input class="w3-radio" type="radio" id="sync-never" name="sync-scheme" value="never" required>Never Sync</label>
|
||||
<p>App will never automatically sync. Reload the page to sync the latest cluster state.</p>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>App Sync Frequency</legend>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 32 B |
@@ -0,0 +1 @@
|
||||
../../common/config-inactive.svg
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 32 B |
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
Vendored
+2
-2
File diff suppressed because one or more lines are too long
@@ -83,7 +83,7 @@ class BackupCard extends HTMLElement {
|
||||
|
||||
async handleDeleteButton () {
|
||||
const template = this.shadowRoot.querySelector("#delete-dialog");
|
||||
dialog(template, async (result, form) => {
|
||||
dialog(template, async (result, _form) => {
|
||||
if (result === "confirm") {
|
||||
const body = {
|
||||
volid: this.volid
|
||||
@@ -99,7 +99,7 @@ class BackupCard extends HTMLElement {
|
||||
|
||||
async handleRestoreButton () {
|
||||
const template = this.shadowRoot.querySelector("#restore-dialog");
|
||||
dialog(template, async (result, form) => {
|
||||
dialog(template, async (result, _form) => {
|
||||
if (result === "confirm") {
|
||||
const body = {
|
||||
volid: this.volid
|
||||
|
||||
@@ -2,8 +2,10 @@ import { getSyncSettings, requestAPI } from "./utils.js";
|
||||
|
||||
export async function setupClientSync (callback) {
|
||||
const { scheme, rate } = getSyncSettings();
|
||||
|
||||
if (scheme === "always") {
|
||||
if (scheme === "never") {
|
||||
return;
|
||||
}
|
||||
else if (scheme === "always") {
|
||||
window.setInterval(callback, rate * 1000);
|
||||
}
|
||||
else if (scheme === "hash") {
|
||||
@@ -19,7 +21,7 @@ export async function setupClientSync (callback) {
|
||||
}
|
||||
else if (scheme === "interrupt") {
|
||||
const socket = new WebSocket(`wss://${window.API.replace("https://", "")}/sync/interrupt`);
|
||||
socket.addEventListener("open", (event) => {
|
||||
socket.addEventListener("open", (_event) => {
|
||||
socket.send(`rate ${rate}`);
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
|
||||
@@ -54,7 +54,7 @@ class VolumeAction extends HTMLElement {
|
||||
|
||||
async handleDiskDetach () {
|
||||
const disk = this.dataset.volume;
|
||||
dialog(this.template, async (result, form) => {
|
||||
dialog(this.template, async (result, _form) => {
|
||||
if (result === "confirm") {
|
||||
this.setStatusLoading();
|
||||
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/detach`, "POST");
|
||||
@@ -136,7 +136,7 @@ class VolumeAction extends HTMLElement {
|
||||
|
||||
async handleDiskDelete () {
|
||||
const disk = this.dataset.volume;
|
||||
dialog(this.template, async (result, form) => {
|
||||
dialog(this.template, async (result, _form) => {
|
||||
if (result === "confirm") {
|
||||
this.setStatusLoading();
|
||||
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/delete`, "DELETE");
|
||||
@@ -224,7 +224,7 @@ async function handleCDAdd () {
|
||||
const isos = await requestAPI("/user/vm-isos", "GET");
|
||||
const select = d.querySelector("#iso-select");
|
||||
|
||||
for (const iso of isos) {
|
||||
for (const iso of isos.data) {
|
||||
select.add(new Option(iso.name, iso.volid));
|
||||
}
|
||||
select.selectedIndex = -1;
|
||||
@@ -275,7 +275,7 @@ class NetworkAction extends HTMLElement {
|
||||
|
||||
async handleNetworkDelete () {
|
||||
const netID = this.dataset.network;
|
||||
dialog(this.template, async (result, form) => {
|
||||
dialog(this.template, async (result, _form) => {
|
||||
if (result === "confirm") {
|
||||
setIconSrc(document.querySelector(`svg[data-network="${netID}"]`), "images/status/loading.svg");
|
||||
const net = `${netID}`;
|
||||
@@ -375,7 +375,7 @@ class DeviceAction extends HTMLElement {
|
||||
|
||||
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
|
||||
d.querySelector("#device").append(new Option(deviceName, deviceDetails.split(",")[0]));
|
||||
for (const availDevice of availDevices) {
|
||||
for (const availDevice of availDevices.data) {
|
||||
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.device_bus));
|
||||
}
|
||||
d.querySelector("#pcie").checked = deviceDetails.includes("pcie=1");
|
||||
@@ -383,7 +383,7 @@ class DeviceAction extends HTMLElement {
|
||||
|
||||
async handleDeviceDelete () {
|
||||
const deviceID = this.dataset.device;
|
||||
dialog(this.template, async (result, form) => {
|
||||
dialog(this.template, async (result, _form) => {
|
||||
if (result === "confirm") {
|
||||
this.setStatusLoading();
|
||||
const device = `${deviceID}`;
|
||||
@@ -437,8 +437,8 @@ async function handleDeviceAdd () {
|
||||
}
|
||||
});
|
||||
|
||||
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
|
||||
for (const availDevice of availDevices) {
|
||||
const availDevices = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci`, "GET");
|
||||
for (const availDevice of availDevices.data) {
|
||||
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.device_bus));
|
||||
}
|
||||
d.querySelector("#pcie").checked = true;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* body contains an optional form or other information,
|
||||
* and controls contains a series of buttons which controls the form
|
||||
*/
|
||||
export function dialog (template, onclose = async (result, form) => { }) {
|
||||
export function dialog (template, onclose = async (_result, _form) => { }) {
|
||||
const dialog = template.content.querySelector("dialog").cloneNode(true);
|
||||
document.body.append(dialog);
|
||||
dialog.addEventListener("close", async () => {
|
||||
@@ -48,7 +48,7 @@ export function alert (message) {
|
||||
dialog.id = "alert-dialog";
|
||||
dialog.innerHTML = `
|
||||
<form method="dialog">
|
||||
<p class="w3-center" style="margin-bottom: 0px;">${message}</p>
|
||||
<p class="w3-center" style="margin-bottom: 0;">${message}</p>
|
||||
<div class="w3-center">
|
||||
<button class="w3-button w3-margin" id="submit">OK</button>
|
||||
</div>
|
||||
@@ -82,18 +82,18 @@ class ErrorDialog extends HTMLElement {
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<style>
|
||||
#errors {
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0;
|
||||
max-height: 20lh;
|
||||
min-height: 20lh;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#errors * {
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<form method="dialog">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">Error</p>
|
||||
<p class="w3-large" id="prompt">Error</p>
|
||||
<div id="errors" class="flex column-reverse"></div>
|
||||
<div class="w3-center" id="controls">
|
||||
<button class="w3-button w3-margin" type="submit" value="ok">OK</button>
|
||||
@@ -113,8 +113,7 @@ class ErrorDialog extends HTMLElement {
|
||||
}
|
||||
|
||||
this.dialog.addEventListener("close", () => {
|
||||
if (this.dialog.returnValue === "ok") {}
|
||||
else if (this.dialog.returnValue === "copy") {
|
||||
if (this.dialog.returnValue === "copy") {
|
||||
let errors = "";
|
||||
for (const error of this.errors.childNodes) {
|
||||
errors += `${error.innerText}\n`;
|
||||
|
||||
@@ -13,7 +13,7 @@ class DraggableContainer extends HTMLElement {
|
||||
window.Sortable.create(this.content, {
|
||||
group: this.dataset.group,
|
||||
ghostClass: "ghost",
|
||||
setData: function (dataTransfer, dragEl) {
|
||||
setData: function (dataTransfer, _dragEl) {
|
||||
dataTransfer.setDragImage(blank, 0, 0);
|
||||
}
|
||||
});
|
||||
|
||||
+47
-51
@@ -159,7 +159,7 @@ class InstanceCard extends HTMLElement {
|
||||
async handlePowerButton () {
|
||||
if (!this.actionLock) {
|
||||
const template = this.shadowRoot.querySelector("#power-dialog");
|
||||
dialog(template, async (result, form) => {
|
||||
dialog(template, async (result, _form) => {
|
||||
if (result === "confirm") {
|
||||
this.actionLock = true;
|
||||
const targetAction = this.status === "running" ? "stop" : "start";
|
||||
@@ -193,7 +193,7 @@ class InstanceCard extends HTMLElement {
|
||||
handleDeleteButton () {
|
||||
if (!this.actionLock && this.status === "stopped") {
|
||||
const template = this.shadowRoot.querySelector("#delete-dialog");
|
||||
dialog(template, async (result, form) => {
|
||||
dialog(template, async (result, _form) => {
|
||||
if (result === "confirm") {
|
||||
this.actionLock = true;
|
||||
|
||||
@@ -247,7 +247,7 @@ function sortInstances () {
|
||||
const searchQuery = document.querySelector("#search").value || null;
|
||||
let criteria;
|
||||
if (!searchQuery) {
|
||||
criteria = (item, query = null) => {
|
||||
criteria = (item, _query = null) => {
|
||||
return { score: item.vmid, alignment: null };
|
||||
};
|
||||
}
|
||||
@@ -343,10 +343,10 @@ async function handleInstanceAddButton () {
|
||||
}
|
||||
});
|
||||
|
||||
const templates = await requestAPI("/user/ct-templates", "GET");
|
||||
|
||||
// setup type select
|
||||
const typeSelect = d.querySelector("#type");
|
||||
typeSelect.selectedIndex = -1;
|
||||
// on type change, reveal or hide the container specific section
|
||||
typeSelect.addEventListener("change", () => {
|
||||
if (typeSelect.value === "qemu") {
|
||||
d.querySelectorAll(".container-specific").forEach((element) => {
|
||||
@@ -366,66 +366,62 @@ async function handleInstanceAddButton () {
|
||||
element.disabled = true;
|
||||
});
|
||||
|
||||
const rootfsContent = "rootdir";
|
||||
const rootfsStorage = d.querySelector("#rootfs-storage");
|
||||
rootfsStorage.selectedIndex = -1;
|
||||
|
||||
const userResources = await requestAPI("/user/dynamic/resources", "GET");
|
||||
const userCluster = await requestAPI("/user/config/cluster", "GET");
|
||||
|
||||
const nodeSelect = d.querySelector("#node");
|
||||
nodeSelect.innerHTML = "";
|
||||
const clusterNodes = await requestPVE("/nodes", "GET");
|
||||
const allowedNodes = Object.keys(userCluster.nodes);
|
||||
clusterNodes.data.forEach((element) => {
|
||||
if (element.status === "online" && allowedNodes.includes(element.node)) {
|
||||
nodeSelect.add(new Option(element.node));
|
||||
}
|
||||
// setup pool select
|
||||
const poolSelect = d.querySelector("#pool");
|
||||
poolSelect.innerHTML = "";
|
||||
// add user pools to selector
|
||||
const userPools = Object.keys((await requestAPI("/access/pools", "GET")).data.pools);
|
||||
userPools.forEach((element) => {
|
||||
poolSelect.add(new Option(element));
|
||||
});
|
||||
poolSelect.selectedIndex = -1;
|
||||
// on pool change, get the allowed nodes for that pool, then repopulate the node selector
|
||||
poolSelect.addEventListener("change", async () => {
|
||||
const pool = (await requestAPI(`/access/pools/${poolSelect.value}`, "GET")).data.pool;
|
||||
|
||||
const nodeSelect = d.querySelector("#node");
|
||||
nodeSelect.innerHTML = "";
|
||||
const clusterNodes = (await requestPVE("/nodes", "GET")).data;
|
||||
const allowedNodes = Object.keys(pool["nodes-allowed"]);
|
||||
clusterNodes.forEach((element) => {
|
||||
if (element.status === "online" && allowedNodes.includes(element.node)) {
|
||||
nodeSelect.add(new Option(element.node));
|
||||
}
|
||||
});
|
||||
nodeSelect.selectedIndex = -1;
|
||||
|
||||
// set vmid min/max
|
||||
d.querySelector("#vmid").min = pool["vmid-allowed"].min;
|
||||
d.querySelector("#vmid").max = pool["vmid-allowed"].max;
|
||||
});
|
||||
|
||||
// setup node select
|
||||
const nodeSelect = d.querySelector("#node");
|
||||
nodeSelect.selectedIndex = -1;
|
||||
// on node change, get the available storages and repopulate the storage selector
|
||||
nodeSelect.addEventListener("change", async () => { // change rootfs storage based on node
|
||||
const node = nodeSelect.value;
|
||||
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
||||
const storage = (await requestPVE(`/nodes/${node}/storage`, "GET")).data;
|
||||
rootfsStorage.innerHTML = "";
|
||||
storage.data.forEach((element) => {
|
||||
storage.forEach((element) => {
|
||||
if (element.content.includes(rootfsContent)) {
|
||||
rootfsStorage.add(new Option(element.storage));
|
||||
}
|
||||
});
|
||||
rootfsStorage.selectedIndex = -1;
|
||||
|
||||
// set core and memory min/max depending on node selected
|
||||
if (node in userResources.cores.nodes) {
|
||||
d.querySelector("#cores").max = userResources.cores.nodes[node].avail;
|
||||
}
|
||||
else {
|
||||
d.querySelector("#cores").max = userResources.cores.global.avail;
|
||||
}
|
||||
|
||||
if (node in userResources.memory.nodes) {
|
||||
d.querySelector("#memory").max = userResources.memory.nodes[node].avail;
|
||||
}
|
||||
else {
|
||||
d.querySelector("#memory").max = userResources.memory.global.avail;
|
||||
}
|
||||
});
|
||||
|
||||
// set vmid min/max
|
||||
d.querySelector("#vmid").min = userCluster.vmid.min;
|
||||
d.querySelector("#vmid").max = userCluster.vmid.max;
|
||||
|
||||
// add user pools to selector
|
||||
const poolSelect = d.querySelector("#pool");
|
||||
poolSelect.innerHTML = "";
|
||||
const userPools = Object.keys(userCluster.pools);
|
||||
userPools.forEach((element) => {
|
||||
poolSelect.add(new Option(element));
|
||||
});
|
||||
poolSelect.selectedIndex = -1;
|
||||
// setup root dir select
|
||||
const rootfsStorage = d.querySelector("#rootfs-storage");
|
||||
rootfsStorage.selectedIndex = -1;
|
||||
// set rootfs content type (rootdir)
|
||||
const rootfsContent = "rootdir";
|
||||
|
||||
// setup templateImage depending on selected image storage
|
||||
const templateImage = d.querySelector("#template-image");
|
||||
// add template images to selector
|
||||
const templateImage = d.querySelector("#template-image"); // populate templateImage depending on selected image storage
|
||||
for (const template of templates) {
|
||||
const templates = await requestAPI("/user/ct-templates", "GET");
|
||||
for (const template of templates.data) {
|
||||
templateImage.append(new Option(template.name, template.volid));
|
||||
}
|
||||
templateImage.selectedIndex = -1;
|
||||
|
||||
@@ -4,6 +4,7 @@ window.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
function init () {
|
||||
setAppearance();
|
||||
|
||||
const { scheme, rate } = getSyncSettings();
|
||||
if (scheme) {
|
||||
document.querySelector(`#sync-${scheme}`).checked = true;
|
||||
|
||||
+13
-12
@@ -80,33 +80,34 @@ async function request (url, content) {
|
||||
try {
|
||||
const response = await fetch(url, content);
|
||||
const contentType = response.headers.get("Content-Type");
|
||||
let data = null;
|
||||
const res = {};
|
||||
|
||||
if (contentType === null) {
|
||||
data = {};
|
||||
res.data = null;
|
||||
res.status = response.status;
|
||||
}
|
||||
else if (contentType.includes("application/json")) {
|
||||
data = await response.json();
|
||||
data.status = response.status;
|
||||
res.data = await response.json();
|
||||
res.status = response.status;
|
||||
}
|
||||
else if (contentType.includes("text/html")) {
|
||||
data = { data: await response.text() };
|
||||
data.status = response.status;
|
||||
res.data = await response.text();
|
||||
res.status = response.status;
|
||||
}
|
||||
else if (contentType.includes("text/plain")) {
|
||||
data = { data: await response.text() };
|
||||
data.status = response.status;
|
||||
res.data = await response.text();
|
||||
res.status = response.status;
|
||||
}
|
||||
else {
|
||||
data = {};
|
||||
res.data = null;
|
||||
res.status = response.status;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
return { status: response.status, error: data ? data.error : response.status };
|
||||
return { status: response.status, error: res.data ? res.data.error : response.status };
|
||||
}
|
||||
else {
|
||||
data.status = response.status;
|
||||
return data || response;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
a {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="w3-row" style="margin-top: 1em; margin-bottom: 1em;">
|
||||
@@ -29,7 +29,7 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Edit Backup
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -39,8 +39,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -49,7 +49,7 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Delete Backup
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -60,8 +60,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -70,7 +70,7 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Restore From Backup?
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -84,8 +84,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -103,7 +103,7 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Create Backup
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -113,8 +113,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
+17
-13
@@ -1,3 +1,4 @@
|
||||
{{/* <head> common across all pages*/}}
|
||||
{{define "head"}}
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -14,18 +15,21 @@
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
{{end}}
|
||||
|
||||
{{/* <header> common across all pages*/}}
|
||||
{{define "header"}}
|
||||
<h1>{{.global.Organization}}</h1>
|
||||
<label for="navtoggle">☰</label>
|
||||
<input type="checkbox" id="navtoggle">
|
||||
<nav id="navigation">
|
||||
{{if eq .page "login"}}
|
||||
<a href="login" aria-current="page">Login</a>
|
||||
{{else}}
|
||||
<a href="index" {{if eq .page "index"}} aria-current="page" {{end}}>Instances</a>
|
||||
<a href="account" {{if eq .page "account"}} aria-current="page" {{end}}>Account</a>
|
||||
<a href="settings" {{if eq .page "settings"}} aria-current="page" {{end}}>Settings</a>
|
||||
<a href="login">Logout</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
<header>
|
||||
<h1>{{.global.Organization}}</h1>
|
||||
<label for="navtoggle">☰</label>
|
||||
<input type="checkbox" id="navtoggle">
|
||||
<nav id="navigation">
|
||||
{{if eq .page "login"}}
|
||||
<a href="login" aria-current="page">Login</a>
|
||||
{{else}}
|
||||
<a href="index" {{if eq .page "index"}} aria-current="page" {{end}}>Instances</a>
|
||||
<a href="account" {{if eq .page "account"}} aria-current="page" {{end}}>Account</a>
|
||||
<a href="settings" {{if eq .page "settings"}} aria-current="page" {{end}}>Settings</a>
|
||||
<a href="login">Logout</a>
|
||||
{{end}}
|
||||
</nav>
|
||||
</header>
|
||||
{{end}}
|
||||
@@ -50,7 +50,7 @@
|
||||
</button>
|
||||
<template id="add-disk-dialog">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Create New Disk
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -65,8 +65,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -79,7 +79,7 @@
|
||||
</button>
|
||||
<template id="add-cd-dialog">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Mount a CDROM
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -89,8 +89,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -163,7 +163,7 @@
|
||||
<img class="clickable" alt="Move {{.Name}}" src="images/actions/disk/move-active.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Move {{.Name}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -173,8 +173,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -198,7 +198,7 @@
|
||||
<img class="clickable" alt="Resize {{.Name}}" src="images/actions/disk/resize-active.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Resize {{.Name}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -208,8 +208,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -233,7 +233,7 @@
|
||||
<img class="clickable" alt="Delete {{.Name}}" src="images/actions/disk/delete-active.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Delete {{.Name}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -242,8 +242,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -267,7 +267,7 @@
|
||||
<img class="clickable" alt="Attach {{.Name}}" src="images/actions/disk/attach.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Attach {{.Name}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -284,8 +284,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -300,7 +300,7 @@
|
||||
<img class="clickable" alt="Detach {{.Name}}" src="images/actions/disk/detach.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Detach {{.Name}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -309,8 +309,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -340,7 +340,7 @@
|
||||
</button>
|
||||
<template id="add-net-dialog">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Create Network Interface
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -353,8 +353,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -371,7 +371,7 @@
|
||||
<img class="clickable" alt="Configure Net {{.Net_ID}}" src="images/actions/network/config.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Edit {{.Net_ID}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -380,8 +380,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -393,7 +393,7 @@
|
||||
<img class="clickable" alt="Delete Net {{.Net_ID}}" src="images/actions/network/delete-active.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Delete {{.Net_ID}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -402,8 +402,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -425,7 +425,7 @@
|
||||
</button>
|
||||
<template id="add-device-dialog">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Add Expansion Card
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -436,8 +436,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -448,13 +448,13 @@
|
||||
<p>{{.Device_ID}}</p>
|
||||
<p>{{.Device_Name}}</p>
|
||||
<div>
|
||||
<device-action data-type="config" data-device="{{.Device_ID}}" data-value="{{.Value}}">
|
||||
<device-action data-type="config" data-device="{{.Device_ID}}" data-value="{{.Device_ID}}">
|
||||
<template shadowrootmode="open">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<img class="clickable" alt="Configure Device {{.Device_ID}}" src="images/actions/device/config.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Edit Expansion Card {{.Device_ID}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -463,20 +463,20 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
</template>
|
||||
</device-action>
|
||||
<device-action data-type="delete" data-device="{{.Device_ID}}" data-value="{{.Value}}">
|
||||
<device-action data-type="delete" data-device="{{.Device_ID}}" data-value="{{.Device_ID}}">
|
||||
<template shadowrootmode="open">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<img class="clickable" alt="Delete Device {{.Device_ID}}" src="images/actions/device/delete-active.svg#symb">
|
||||
<template id="dialog-template">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
remove Expansion Card {{.Device_ID}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -485,8 +485,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -516,10 +516,17 @@
|
||||
div.draggable-item {
|
||||
cursor: grab;
|
||||
}
|
||||
div.draggable-item svg {
|
||||
div.draggable-item img {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
}
|
||||
div.draggable-item p {
|
||||
margin: 0;
|
||||
}
|
||||
div.draggable-item p.volume-file {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#wrapper {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
@@ -536,18 +543,18 @@
|
||||
|
||||
{{define "boot-target"}}
|
||||
{{if .volume_id}}
|
||||
<div class="draggable-item" data-value="{{.volume_id}}" style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 10px; align-items: center;">
|
||||
<img style="height: 1em; width: 1em" alt="Drag" src="images/actions/drag.svg#symb">
|
||||
<img style="height: 1em; width: 1em" alt="Volume" src="images/resources/drive.svg#symb">
|
||||
<p style="margin: 0px;">{{.volume_id}}</p>
|
||||
<p style="margin: 0px; overflow: hidden; white-space: nowrap;">{{.file}}</p>
|
||||
<div class="draggable-item" data-value="{{.volume_id}}" style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 0.5em; align-items: center;">
|
||||
<img alt="Drag" src="images/actions/drag.svg#symb">
|
||||
<img alt="Volume" src="images/resources/drive.svg#symb">
|
||||
<p class="volume-id">{{.volume_id}}</p>
|
||||
<p class="volume-file">{{.file}}</p>
|
||||
</div>
|
||||
{{else if .net_id}}
|
||||
<div class="draggable-item" data-value="{{.net_id}}" style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 10px; align-items: center;">
|
||||
<img style="height: 1em; width: 1em" alt="Drag" src="images/actions/drag.svg#symb">
|
||||
<img style="height: 1em; width: 1em" alt="Net" src="images/resources/network.svg#symb">
|
||||
<p style="margin: 0px;">{{.net_id}}</p>
|
||||
<p style="margin: 0px; overflow: hidden; white-space: nowrap;">{{.value}}</p>
|
||||
<div class="draggable-item" data-value="{{.net_id}}" style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 0.5em; align-items: center;">
|
||||
<img alt="Drag" src="images/actions/drag.svg#symb">
|
||||
<img alt="Net" src="images/resources/network.svg#symb">
|
||||
<p class="volume-id">{{.net_id}}</p>
|
||||
<p class="volume-file">{{.value}}</p>
|
||||
</div>
|
||||
{{else}}
|
||||
{{end}}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
line-height: 1em;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
a img {
|
||||
vertical-align: unset;
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
.row { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
||||
flex-direction: row;
|
||||
column-gap: 10px;
|
||||
column-gap: 0.5em;
|
||||
align-items: center;
|
||||
}
|
||||
.nowrap { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
||||
@@ -43,7 +43,7 @@
|
||||
.hide-large {display: none !important;}
|
||||
.hide-medium {display:none !important}
|
||||
}
|
||||
@media screen and (width <=601px) {
|
||||
@media screen and (width <=601px) and (width >=440px){
|
||||
.hide-large {display: none !important;}
|
||||
.hide-medium {display:none !important}
|
||||
.hide-small {display:none !important}
|
||||
@@ -82,7 +82,7 @@
|
||||
{{end}}
|
||||
<p>{{.NodeStatus}}</p>
|
||||
</div>
|
||||
<div class="flex row nowrap" style="height: 1lh;">
|
||||
<div class="flex row nowrap">
|
||||
{{if and (eq .NodeStatus "online") (eq .Status "running")}}
|
||||
<img id="power-btn" class="clickable" alt="shutdown instance" role="button" tabindex=0 src="images/actions/instance/stop.svg#symb">
|
||||
<img id="configure-btn" alt="" aria-disabled="true" role="none" src="images/actions/instance/config-inactive.svg#symb">
|
||||
@@ -115,7 +115,7 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
{{if eq .Status "running"}}
|
||||
Stop {{.VMID}}
|
||||
{{else if eq .Status "stopped"}}
|
||||
@@ -134,8 +134,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
@@ -144,7 +144,7 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<link rel="stylesheet" href="css/form.css">
|
||||
<dialog class="w3-container w3-card w3-border-0">
|
||||
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||
<p class="w3-large" id="prompt">
|
||||
Delete {{.VMID}}
|
||||
</p>
|
||||
<div id="body">
|
||||
@@ -153,8 +153,8 @@
|
||||
</form>
|
||||
</div>
|
||||
<div id="controls" class="w3-center w3-container">
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin">CONFIRM</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{{define "pool-resources"}}
|
||||
<section class="w3-card w3-padding">
|
||||
<h3>Pool: {{.PoolID}}</h3>
|
||||
<p id="vmid">VMID Range: {{.AllowedVMIDRange.Min}} - {{.AllowedVMIDRange.Max}}</p>
|
||||
<p id="nodes">Nodes: {{MapKeys .AllowedNodes ", "}}</p>
|
||||
<p id="backups">Max Backups Per Instance: {{.AllowedBackups.MaxPerInstance}} Max Backups Total: {{.AllowedBackups.MaxTotal}}</p>
|
||||
<div>
|
||||
{{range $category, $v := .Resources}}
|
||||
{{if eq $category ""}}
|
||||
<h4>Generic</h4>
|
||||
{{else}}
|
||||
<h4>{{$category}}</h4>
|
||||
{{end}}
|
||||
<div class="resource-container">
|
||||
{{range $v}}
|
||||
{{if .Display}}
|
||||
{{if eq .Type "numeric"}}
|
||||
{{template "resource-chart" .}}
|
||||
{{end}}
|
||||
{{if eq .Type "storage"}}
|
||||
{{template "resource-chart" .}}
|
||||
{{end}}
|
||||
{{if eq .Type "list"}}
|
||||
{{range .Resources}}
|
||||
{{template "resource-chart" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -8,8 +8,7 @@
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
}
|
||||
progress {
|
||||
width: 100%;
|
||||
@@ -19,7 +18,7 @@
|
||||
}
|
||||
#caption {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -37,7 +36,7 @@
|
||||
<progress value="{{.Used}}" max="{{.Max}}" id="resource"></progress>
|
||||
<label id="caption" for="resource">
|
||||
<span>{{.Name}}</span>
|
||||
<span>{{printf "%g" .Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
|
||||
<span>{{.Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
{{/*
|
||||
Select: generic data driven <select> element template
|
||||
.ID = (string) select element id & name attribute
|
||||
.Required = (bool) select element required attribute
|
||||
.Options = ([]Options) array of Options
|
||||
*/}}
|
||||
{{define "select"}}
|
||||
<select class="w3-select w3-border" id="{{.ID}}" name="{{.ID}}" {{if .Required}}required{{end}}>
|
||||
{{range .Options}}
|
||||
{{range .Options}}
|
||||
{{template "option" .}}
|
||||
{{end}}
|
||||
</select>
|
||||
{{end}}
|
||||
|
||||
{{/*
|
||||
Options: generic data driven <option> element template
|
||||
.Selected = (bool) option element selected attribute
|
||||
.Value = (string) option element value attribute
|
||||
.Display = (string) option element innerText
|
||||
*/}}
|
||||
{{define "option"}}
|
||||
{{if .Selected}}
|
||||
<option value="{{.Value}}" selected>{{.Display}}</option>
|
||||
{{else}}
|
||||
<option value="{{.Value}}">{{.Display}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user