Compare commits
57 Commits
2b5c1bbf11
...
main
Author | SHA1 | Date | |
---|---|---|---|
7db0bea35c | |||
ff98eb318e | |||
05ced39598 | |||
4e2b6278d8 | |||
75e098b7b4 | |||
8c378a3b49 | |||
3d5989a946 | |||
06afdcec37 | |||
2f21b23535 | |||
e8dd28b519 | |||
e0c7a53d85 | |||
118b7dac53 | |||
db32f318b9 | |||
c13a4c8539 | |||
8d490cd336 | |||
343c149330 | |||
d95a82f248 | |||
fc42de2c49 | |||
3f723394c4 | |||
e7627b5787 | |||
7732da0642 | |||
87c42495ad | |||
89065254d2 | |||
08e5f8b392 | |||
8be935a421 | |||
f94dca7e0c | |||
8905886065 | |||
df6772c72b | |||
f3b6c0abf4 | |||
69fae92313 | |||
a79dd96d2a | |||
ee397c48e1 | |||
33b0a4b5ff | |||
65c8fbdca8 | |||
e932165a98 | |||
8c339794b3 | |||
a62fc83386 | |||
756aef587d | |||
ca555a7116 | |||
85c3ab49fc | |||
9ec277ce65 | |||
e41c8d2a07 | |||
308d133e6e | |||
99d58eb250 | |||
acd6eba520 | |||
478ca20451 | |||
28c60aecc9 | |||
3d677a46ee | |||
e170d7f93d | |||
85bd81ef30 | |||
53832b67a2 | |||
e6cd1fbb3d | |||
3f21f3c4a4 | |||
1bcbed6828 | |||
31bfa79e66 | |||
989f59223a | |||
233d4255ba |
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "ProxmoxAAS-Fabric"]
|
||||||
|
path = ProxmoxAAS-Fabric
|
||||||
|
url = https://git.tronnet.net/tronnet/ProxmoxAAS-Fabric
|
1
ProxmoxAAS-Fabric
Submodule
15
app/app.go
@@ -1,40 +1,37 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"proxmoxaas-dashboard/dist/web" // go will complain here until the first build
|
|
||||||
|
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
"proxmoxaas-dashboard/app/routes"
|
"proxmoxaas-dashboard/app/routes"
|
||||||
|
"proxmoxaas-dashboard/dist/web" // go will complain here until the first build
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/tdewolff/minify/v2"
|
"github.com/tdewolff/minify/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run() {
|
func Run(configPath *string) {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
|
|
||||||
configPath := flag.String("config", "config.json", "path to config.json file")
|
|
||||||
flag.Parse()
|
|
||||||
common.Global = common.GetConfig(*configPath)
|
common.Global = common.GetConfig(*configPath)
|
||||||
|
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
m := common.InitMinify()
|
m := common.InitMinify()
|
||||||
ServeStatic(router, m)
|
ServeStatic(router, m)
|
||||||
html := common.MinifyStatic(m, web.Templates)
|
html := common.MinifyStatic(m, web.Templates)
|
||||||
common.TMPL = common.LoadHTMLToGin(router, html)
|
common.TMPL = common.LoadHTMLToGin(router, html)
|
||||||
|
|
||||||
router.GET("/account", routes.HandleGETAccount)
|
|
||||||
router.GET("/", routes.HandleGETIndex)
|
router.GET("/", routes.HandleGETIndex)
|
||||||
router.GET("/index", routes.HandleGETIndex)
|
router.GET("/index", routes.HandleGETIndex)
|
||||||
router.GET("/index/instances", routes.HandleGETInstancesFragment)
|
router.GET("/index/instances", routes.HandleGETInstancesFragment)
|
||||||
|
router.GET("/account", routes.HandleGETAccount)
|
||||||
router.GET("/config", routes.HandleGETConfig)
|
router.GET("/config", routes.HandleGETConfig)
|
||||||
router.GET("/config/volumes", routes.HandleGETConfigVolumesFragment)
|
router.GET("/config/volumes", routes.HandleGETConfigVolumesFragment)
|
||||||
router.GET("/config/nets", routes.HandleGETConfigNetsFragment)
|
router.GET("/config/nets", routes.HandleGETConfigNetsFragment)
|
||||||
router.GET("/config/devices", routes.HandleGETConfigDevicesFragment)
|
router.GET("/config/devices", routes.HandleGETConfigDevicesFragment)
|
||||||
router.GET("/config/boot", routes.HandleGETConfigBootFragment)
|
router.GET("/config/boot", routes.HandleGETConfigBootFragment)
|
||||||
|
router.GET("/backups", routes.HandleGETBackups)
|
||||||
|
router.GET("/backups/backups", routes.HandleGETBackupsFragment)
|
||||||
router.GET("/login", routes.HandleGETLogin)
|
router.GET("/login", routes.HandleGETLogin)
|
||||||
router.GET("/settings", routes.HandleGETSettings)
|
router.GET("/settings", routes.HandleGETSettings)
|
||||||
|
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/tdewolff/minify/v2/css"
|
"github.com/tdewolff/minify/v2/css"
|
||||||
"github.com/tdewolff/minify/v2/html"
|
"github.com/tdewolff/minify/v2/html"
|
||||||
"github.com/tdewolff/minify/v2/js"
|
"github.com/tdewolff/minify/v2/js"
|
||||||
|
"github.com/tdewolff/minify/v2/svg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// defines mime type and associated minifier
|
// defines mime type and associated minifier
|
||||||
@@ -35,7 +36,7 @@ var MimeTypes = map[string]MimeType{
|
|||||||
},
|
},
|
||||||
"svg": {
|
"svg": {
|
||||||
Type: "image/svg+xml",
|
Type: "image/svg+xml",
|
||||||
Minifier: nil,
|
Minifier: svg.Minify,
|
||||||
},
|
},
|
||||||
"js": {
|
"js": {
|
||||||
Type: "application/javascript",
|
Type: "application/javascript",
|
||||||
@@ -50,3 +51,41 @@ var MimeTypes = map[string]MimeType{
|
|||||||
Minifier: nil,
|
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,
|
||||||
|
},
|
||||||
|
"js": {
|
||||||
|
Type: "application/javascript",
|
||||||
|
Minifier: nil,
|
||||||
|
},
|
||||||
|
"wasm": {
|
||||||
|
Type: "application/wasm",
|
||||||
|
Minifier: nil,
|
||||||
|
},
|
||||||
|
"*": {
|
||||||
|
Type: "text/plain",
|
||||||
|
Minifier: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
@@ -31,7 +31,6 @@ type RequestType int
|
|||||||
|
|
||||||
type RequestContext struct {
|
type RequestContext struct {
|
||||||
Cookies map[string]string
|
Cookies map[string]string
|
||||||
Body map[string]any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Auth struct {
|
type Auth struct {
|
||||||
|
@@ -10,6 +10,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -22,6 +23,12 @@ import (
|
|||||||
var TMPL *template.Template
|
var TMPL *template.Template
|
||||||
var Global Config
|
var Global Config
|
||||||
|
|
||||||
|
type VMPath struct {
|
||||||
|
Node string
|
||||||
|
Type string
|
||||||
|
VMID string
|
||||||
|
}
|
||||||
|
|
||||||
func GetConfig(configPath string) Config {
|
func GetConfig(configPath string) Config {
|
||||||
content, err := os.ReadFile(configPath)
|
content, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -159,7 +166,7 @@ func HandleNonFatalError(c *gin.Context, err error) {
|
|||||||
c.Status(http.StatusInternalServerError)
|
c.Status(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RequestGetAPI(path string, context RequestContext) (*http.Response, int, error) {
|
func RequestGetAPI(path string, context RequestContext, body any) (*http.Response, int, error) {
|
||||||
req, err := http.NewRequest("GET", Global.API+path, nil)
|
req, err := http.NewRequest("GET", Global.API+path, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -186,9 +193,18 @@ func RequestGetAPI(path string, context RequestContext) (*http.Response, int, er
|
|||||||
return nil, response.StatusCode, err
|
return nil, response.StatusCode, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(data, &context.Body)
|
switch body.(type) { // write json to body object depending on type, currently supports map[string]any (ie json) or []any (ie array of json)
|
||||||
if err != nil {
|
case *map[string]any:
|
||||||
return nil, response.StatusCode, err
|
err = json.Unmarshal(data, &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, response.StatusCode, err
|
||||||
|
}
|
||||||
|
case *[]any:
|
||||||
|
err = json.Unmarshal(data, &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, response.StatusCode, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, response.StatusCode, nil
|
return response, response.StatusCode, nil
|
||||||
@@ -205,3 +221,38 @@ func GetAuth(c *gin.Context) (Auth, error) {
|
|||||||
return Auth{username, token, csrf}, nil
|
return Auth{username, token, csrf}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExtractVMPath(c *gin.Context) (VMPath, 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)
|
||||||
|
}
|
||||||
|
vm_path := VMPath{
|
||||||
|
Node: req_node,
|
||||||
|
Type: req_type,
|
||||||
|
VMID: req_vmid,
|
||||||
|
}
|
||||||
|
return vm_path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatNumber(val int64, base int64) (float64, string) {
|
||||||
|
valf := float64(val)
|
||||||
|
basef := float64(base)
|
||||||
|
steps := 0
|
||||||
|
for math.Abs(valf) > basef && steps < 4 {
|
||||||
|
valf /= basef
|
||||||
|
steps++
|
||||||
|
}
|
||||||
|
|
||||||
|
if base == 1000 {
|
||||||
|
prefixes := []string{"", "K", "M", "G", "T"}
|
||||||
|
return valf, prefixes[steps]
|
||||||
|
} else if base == 1024 {
|
||||||
|
prefixes := []string{"", "Ki", "Mi", "Gi", "Ti"}
|
||||||
|
return valf, prefixes[steps]
|
||||||
|
} else {
|
||||||
|
return 0, ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -2,85 +2,14 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
|
|
||||||
|
"github.com/gerow/go-color"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-viper/mapstructure/v2"
|
"github.com/go-viper/mapstructure/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleGETAccount(c *gin.Context) {
|
|
||||||
auth, err := common.GetAuth(c)
|
|
||||||
if err == nil {
|
|
||||||
account, err := GetUserAccount(auth)
|
|
||||||
if err != nil {
|
|
||||||
common.HandleNonFatalError(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range account.Resources {
|
|
||||||
switch t := v.(type) {
|
|
||||||
case NumericResource:
|
|
||||||
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
|
||||||
account.Resources[k] = ResourceChart{
|
|
||||||
Type: t.Type,
|
|
||||||
Display: t.Display,
|
|
||||||
Name: t.Name,
|
|
||||||
Used: t.Total.Used,
|
|
||||||
Max: t.Total.Max,
|
|
||||||
Avail: avail,
|
|
||||||
Prefix: prefix,
|
|
||||||
Unit: t.Unit,
|
|
||||||
}
|
|
||||||
case StorageResource:
|
|
||||||
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
|
||||||
account.Resources[k] = ResourceChart{
|
|
||||||
Type: t.Type,
|
|
||||||
Display: t.Display,
|
|
||||||
Name: t.Name,
|
|
||||||
Used: t.Total.Used,
|
|
||||||
Max: t.Total.Max,
|
|
||||||
Avail: avail,
|
|
||||||
Prefix: prefix,
|
|
||||||
Unit: t.Unit,
|
|
||||||
}
|
|
||||||
case ListResource:
|
|
||||||
l := struct {
|
|
||||||
Type string
|
|
||||||
Display bool
|
|
||||||
Resources []ResourceChart
|
|
||||||
}{
|
|
||||||
Type: t.Type,
|
|
||||||
Display: t.Display,
|
|
||||||
Resources: []ResourceChart{},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range t.Total {
|
|
||||||
l.Resources = append(l.Resources, ResourceChart{
|
|
||||||
Type: t.Type,
|
|
||||||
Display: t.Display,
|
|
||||||
Name: r.Name,
|
|
||||||
Used: r.Used,
|
|
||||||
Max: r.Max,
|
|
||||||
Avail: float64(r.Avail), // usually an int
|
|
||||||
Unit: "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
account.Resources[k] = l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "html/account.html", gin.H{
|
|
||||||
"global": common.Global,
|
|
||||||
"page": "account",
|
|
||||||
"account": account,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Username string
|
Username string
|
||||||
Pools map[string]bool
|
Pools map[string]bool
|
||||||
@@ -89,7 +18,7 @@ type Account struct {
|
|||||||
Min int
|
Min int
|
||||||
Max int
|
Max int
|
||||||
}
|
}
|
||||||
Resources map[string]any
|
Resources map[string]map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
type Constraint struct {
|
type Constraint struct {
|
||||||
@@ -117,6 +46,7 @@ type NumericResource struct {
|
|||||||
Global Constraint
|
Global Constraint
|
||||||
Nodes map[string]Constraint
|
Nodes map[string]Constraint
|
||||||
Total Constraint
|
Total Constraint
|
||||||
|
Category string
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageResource struct {
|
type StorageResource struct {
|
||||||
@@ -131,6 +61,7 @@ type StorageResource struct {
|
|||||||
Global Constraint
|
Global Constraint
|
||||||
Nodes map[string]Constraint
|
Nodes map[string]Constraint
|
||||||
Total Constraint
|
Total Constraint
|
||||||
|
Category string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListResource struct {
|
type ListResource struct {
|
||||||
@@ -140,22 +71,112 @@ type ListResource struct {
|
|||||||
Global []Match
|
Global []Match
|
||||||
Nodes map[string][]Match
|
Nodes map[string][]Match
|
||||||
Total []Match
|
Total []Match
|
||||||
|
Category string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceChart struct {
|
type ResourceChart struct {
|
||||||
Type string
|
Type string
|
||||||
Display bool
|
Display bool
|
||||||
Name string
|
Name string
|
||||||
Used int64
|
Used int64
|
||||||
Max int64
|
Max int64
|
||||||
Avail float64
|
Avail float64
|
||||||
Prefix string
|
Prefix string
|
||||||
Unit string
|
Unit string
|
||||||
|
ColorHex string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Red = color.RGB{
|
||||||
|
R: 1,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
var Green = color.RGB{
|
||||||
|
R: 0,
|
||||||
|
G: 1,
|
||||||
|
B: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGETAccount(c *gin.Context) {
|
||||||
|
auth, err := common.GetAuth(c)
|
||||||
|
if err == nil {
|
||||||
|
account, err := GetUserAccount(auth)
|
||||||
|
if err != nil {
|
||||||
|
common.HandleNonFatalError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for 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{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range t.Total {
|
||||||
|
l.Resources = append(l.Resources, ResourceChart{
|
||||||
|
Type: t.Type,
|
||||||
|
Display: t.Display,
|
||||||
|
Name: r.Name,
|
||||||
|
Used: r.Used,
|
||||||
|
Max: r.Max,
|
||||||
|
Avail: float64(r.Avail), // usually an int
|
||||||
|
Unit: "",
|
||||||
|
ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
account.Resources[category][resource] = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusOK, "html/account.html", gin.H{
|
||||||
|
"global": common.Global,
|
||||||
|
"page": "account",
|
||||||
|
"account": account,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserAccount(auth common.Auth) (Account, error) {
|
func GetUserAccount(auth common.Auth) (Account, error) {
|
||||||
account := Account{
|
account := Account{
|
||||||
Resources: map[string]any{},
|
Resources: map[string]map[string]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := common.RequestContext{
|
ctx := common.RequestContext{
|
||||||
@@ -164,51 +185,55 @@ func GetUserAccount(auth common.Auth) (Account, error) {
|
|||||||
"PVEAuthCookie": auth.Token,
|
"PVEAuthCookie": auth.Token,
|
||||||
"CSRFPreventionToken": auth.CSRF,
|
"CSRFPreventionToken": auth.CSRF,
|
||||||
},
|
},
|
||||||
Body: map[string]any{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get user account basic data
|
// get user account basic data
|
||||||
res, code, err := common.RequestGetAPI("/user/config/cluster", ctx)
|
body := map[string]any{}
|
||||||
|
res, code, err := common.RequestGetAPI("/user/config/cluster", ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return account, err
|
return account, err
|
||||||
}
|
}
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res)
|
return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res)
|
||||||
}
|
}
|
||||||
err = mapstructure.Decode(ctx.Body, &account)
|
err = mapstructure.Decode(body, &account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return account, err
|
return account, err
|
||||||
} else {
|
} else {
|
||||||
account.Username = auth.Username
|
account.Username = auth.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Body = map[string]any{}
|
body = map[string]any{}
|
||||||
// get user resources
|
// get user resources
|
||||||
res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx)
|
res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return account, err
|
return account, err
|
||||||
}
|
}
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
return account, fmt.Errorf("request to /user/dynamic/resources resulted in %+v", res)
|
return account, fmt.Errorf("request to /user/dynamic/resources resulted in %+v", res)
|
||||||
}
|
}
|
||||||
resources := ctx.Body
|
resources := body
|
||||||
|
|
||||||
ctx.Body = map[string]any{}
|
body = map[string]any{}
|
||||||
// get resource meta data
|
// get resource meta data
|
||||||
res, code, err = common.RequestGetAPI("/global/config/resources", ctx)
|
res, code, err = common.RequestGetAPI("/global/config/resources", ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return account, err
|
return account, err
|
||||||
}
|
}
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
return account, fmt.Errorf("request to /global/config/resources resulted in %+v", res)
|
return account, fmt.Errorf("request to /global/config/resources resulted in %+v", res)
|
||||||
}
|
}
|
||||||
meta := ctx.Body["resources"].(map[string]any)
|
meta := body["resources"].(map[string]any)
|
||||||
|
|
||||||
// build each resource by its meta type
|
// build each resource by its meta type
|
||||||
for k, v := range meta {
|
for k, v := range meta {
|
||||||
m := v.(map[string]any)
|
m := v.(map[string]any)
|
||||||
t := m["type"].(string)
|
t := m["type"].(string)
|
||||||
r := resources[k].(map[string]any)
|
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" {
|
if t == "numeric" {
|
||||||
n := NumericResource{}
|
n := NumericResource{}
|
||||||
n.Type = t
|
n.Type = t
|
||||||
@@ -217,7 +242,7 @@ func GetUserAccount(auth common.Auth) (Account, error) {
|
|||||||
if err_m != nil || err_r != nil {
|
if err_m != nil || err_r != nil {
|
||||||
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
}
|
}
|
||||||
account.Resources[k] = n
|
account.Resources[category][k] = n
|
||||||
} else if t == "storage" {
|
} else if t == "storage" {
|
||||||
n := StorageResource{}
|
n := StorageResource{}
|
||||||
n.Type = t
|
n.Type = t
|
||||||
@@ -226,7 +251,7 @@ func GetUserAccount(auth common.Auth) (Account, error) {
|
|||||||
if err_m != nil || err_r != nil {
|
if err_m != nil || err_r != nil {
|
||||||
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
}
|
}
|
||||||
account.Resources[k] = n
|
account.Resources[category][k] = n
|
||||||
} else if t == "list" {
|
} else if t == "list" {
|
||||||
n := ListResource{}
|
n := ListResource{}
|
||||||
n.Type = t
|
n.Type = t
|
||||||
@@ -235,29 +260,21 @@ func GetUserAccount(auth common.Auth) (Account, error) {
|
|||||||
if err_m != nil || err_r != nil {
|
if err_m != nil || err_r != nil {
|
||||||
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
}
|
}
|
||||||
account.Resources[k] = n
|
account.Resources[category][k] = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return account, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FormatNumber(val int64, base int64) (float64, string) {
|
// interpolate between min and max by normalized (0 - 1) val
|
||||||
valf := float64(val)
|
func InterpolateColorHSV(min color.RGB, max color.RGB, val float64) color.RGB {
|
||||||
basef := float64(base)
|
minhsl := min.ToHSL()
|
||||||
steps := 0
|
maxhsl := max.ToHSL()
|
||||||
for math.Abs(valf) > basef && steps < 4 {
|
interphsl := color.HSL{
|
||||||
valf /= basef
|
H: (1-val)*minhsl.H + (val)*maxhsl.H,
|
||||||
steps++
|
S: (1-val)*minhsl.S + (val)*maxhsl.S,
|
||||||
}
|
L: (1-val)*minhsl.L + (val)*maxhsl.L,
|
||||||
|
|
||||||
if base == 1000 {
|
|
||||||
prefixes := []string{"", "K", "M", "G", "T"}
|
|
||||||
return valf, prefixes[steps]
|
|
||||||
} else if base == 1024 {
|
|
||||||
prefixes := []string{"", "Ki", "Mi", "Gi", "Ti"}
|
|
||||||
return valf, prefixes[steps]
|
|
||||||
} else {
|
|
||||||
return 0, ""
|
|
||||||
}
|
}
|
||||||
|
return interphsl.ToRGB()
|
||||||
}
|
}
|
||||||
|
110
app/routes/backups.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"proxmoxaas-dashboard/app/common"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-viper/mapstructure/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstanceBackup struct {
|
||||||
|
Volid string `json:"volid"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
CTime int64 `json:"ctime"`
|
||||||
|
SizeFormatted string
|
||||||
|
TimeFormatted string
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGETBackups(c *gin.Context) {
|
||||||
|
auth, err := common.GetAuth(c)
|
||||||
|
if err == nil {
|
||||||
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
|
if err != nil {
|
||||||
|
common.HandleNonFatalError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backups, err := GetInstanceBackups(vm_path, auth)
|
||||||
|
if err != nil {
|
||||||
|
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance backups: %s", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := GetInstanceConfig(vm_path, auth) // only used for the VM's name
|
||||||
|
if err != nil {
|
||||||
|
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",
|
||||||
|
"backups": backups,
|
||||||
|
"config": config,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.Redirect(http.StatusFound, "/login")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGETBackupsFragment(c *gin.Context) {
|
||||||
|
auth, err := common.GetAuth(c)
|
||||||
|
if err == nil { // user should be authed, try to return index with population
|
||||||
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
|
if err != nil {
|
||||||
|
common.HandleNonFatalError(c, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backups, err := GetInstanceBackups(vm_path, auth)
|
||||||
|
if err != nil {
|
||||||
|
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance backups: %s", err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "text/plain")
|
||||||
|
common.TMPL.ExecuteTemplate(c.Writer, "html/backups-backups.go.tmpl", gin.H{
|
||||||
|
"backups": backups,
|
||||||
|
})
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
} else { // return 401
|
||||||
|
c.Status(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInstanceBackups(vm common.VMPath, auth common.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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body := []any{}
|
||||||
|
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||||
|
if err != nil {
|
||||||
|
return backups, err
|
||||||
|
}
|
||||||
|
if code != 200 {
|
||||||
|
return backups, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mapstructure.Decode(body, &backups)
|
||||||
|
if err != nil {
|
||||||
|
return backups, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range backups {
|
||||||
|
size, prefix := common.FormatNumber(backups[i].Size, 1024)
|
||||||
|
backups[i].SizeFormatted = fmt.Sprintf("%.3g %sB", size, prefix)
|
||||||
|
|
||||||
|
t := time.Unix(backups[i].CTime, 0)
|
||||||
|
backups[i].TimeFormatted = t.Format("02-01-06 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
|
return backups, nil
|
||||||
|
}
|
@@ -4,19 +4,52 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
|
fabric "proxmoxaas-fabric/app"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
fabric "proxmoxaas-fabric/app"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-viper/mapstructure/v2"
|
"github.com/go-viper/mapstructure/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
// overrides
|
||||||
|
ProctypeSelect common.Select
|
||||||
|
}
|
||||||
|
|
||||||
|
type GlobalConfig struct {
|
||||||
|
CPU struct {
|
||||||
|
Whitelist bool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserConfigResources struct {
|
||||||
|
CPU struct {
|
||||||
|
Global []CPUConfig
|
||||||
|
Nodes map[string][]CPUConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CPUConfig struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
func HandleGETConfig(c *gin.Context) {
|
func HandleGETConfig(c *gin.Context) {
|
||||||
auth, err := common.GetAuth(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := ExtractVMPath(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
}
|
}
|
||||||
@@ -33,7 +66,7 @@ func HandleGETConfig(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, cpu := range config.ProctypeSelect.Options {
|
for i, cpu := range config.ProctypeSelect.Options {
|
||||||
if cpu.Value == config.Proctype {
|
if cpu.Value == config.CPU {
|
||||||
config.ProctypeSelect.Options[i].Selected = true
|
config.ProctypeSelect.Options[i].Selected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +84,7 @@ func HandleGETConfig(c *gin.Context) {
|
|||||||
func HandleGETConfigVolumesFragment(c *gin.Context) {
|
func HandleGETConfigVolumesFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuth(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := ExtractVMPath(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
}
|
}
|
||||||
@@ -62,7 +95,7 @@ func HandleGETConfigVolumesFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
common.TMPL.ExecuteTemplate(c.Writer, "html/config-volumes.frag", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/config-volumes.go.tmpl", gin.H{
|
||||||
"config": config,
|
"config": config,
|
||||||
})
|
})
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
@@ -74,7 +107,7 @@ func HandleGETConfigVolumesFragment(c *gin.Context) {
|
|||||||
func HandleGETConfigNetsFragment(c *gin.Context) {
|
func HandleGETConfigNetsFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuth(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := ExtractVMPath(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
}
|
}
|
||||||
@@ -85,7 +118,7 @@ func HandleGETConfigNetsFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
common.TMPL.ExecuteTemplate(c.Writer, "html/config-nets.frag", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/config-nets.go.tmpl", gin.H{
|
||||||
"config": config,
|
"config": config,
|
||||||
})
|
})
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
@@ -97,7 +130,7 @@ func HandleGETConfigNetsFragment(c *gin.Context) {
|
|||||||
func HandleGETConfigDevicesFragment(c *gin.Context) {
|
func HandleGETConfigDevicesFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuth(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := ExtractVMPath(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
}
|
}
|
||||||
@@ -108,7 +141,7 @@ func HandleGETConfigDevicesFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
common.TMPL.ExecuteTemplate(c.Writer, "html/config-devices.frag", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/config-devices.go.tmpl", gin.H{
|
||||||
"config": config,
|
"config": config,
|
||||||
})
|
})
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
@@ -120,7 +153,7 @@ func HandleGETConfigDevicesFragment(c *gin.Context) {
|
|||||||
func HandleGETConfigBootFragment(c *gin.Context) {
|
func HandleGETConfigBootFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuth(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := ExtractVMPath(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
}
|
}
|
||||||
@@ -131,7 +164,7 @@ func HandleGETConfigBootFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
common.TMPL.ExecuteTemplate(c.Writer, "html/config-boot.frag", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/config-boot.go.tmpl", gin.H{
|
||||||
"config": config,
|
"config": config,
|
||||||
})
|
})
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
@@ -140,45 +173,7 @@ func HandleGETConfigBootFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractVMPath(c *gin.Context) (VMPath, error) {
|
func GetInstanceConfig(vm common.VMPath, auth common.Auth) (InstanceConfig, 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)
|
|
||||||
}
|
|
||||||
vm_path := VMPath{
|
|
||||||
Node: req_node,
|
|
||||||
Type: req_type,
|
|
||||||
VMID: req_vmid,
|
|
||||||
}
|
|
||||||
return vm_path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type VMPath struct {
|
|
||||||
Node string
|
|
||||||
Type string
|
|
||||||
VMID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// imported types from fabric
|
|
||||||
|
|
||||||
type InstanceConfig struct {
|
|
||||||
Type fabric.InstanceType `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Proctype string `json:"cpu"`
|
|
||||||
Cores uint64 `json:"cores"`
|
|
||||||
Memory uint64 `json:"memory"`
|
|
||||||
Swap uint64 `json:"swap"`
|
|
||||||
Volumes map[string]*fabric.Volume `json:"volumes"`
|
|
||||||
Nets map[string]*fabric.Net `json:"nets"`
|
|
||||||
Devices map[string]*fabric.Device `json:"devices"`
|
|
||||||
Boot fabric.BootOrder `json:"boot"`
|
|
||||||
// overrides
|
|
||||||
ProctypeSelect common.Select
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
|
|
||||||
config := InstanceConfig{}
|
config := InstanceConfig{}
|
||||||
path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID)
|
path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID)
|
||||||
ctx := common.RequestContext{
|
ctx := common.RequestContext{
|
||||||
@@ -187,9 +182,9 @@ func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
|
|||||||
"PVEAuthCookie": auth.Token,
|
"PVEAuthCookie": auth.Token,
|
||||||
"CSRFPreventionToken": auth.CSRF,
|
"CSRFPreventionToken": auth.CSRF,
|
||||||
},
|
},
|
||||||
Body: map[string]any{},
|
|
||||||
}
|
}
|
||||||
res, code, err := common.RequestGetAPI(path, ctx)
|
body := map[string]any{}
|
||||||
|
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
@@ -197,7 +192,7 @@ func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
|
|||||||
return config, fmt.Errorf("request to %s resulted in %+v", path, res)
|
return config, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = mapstructure.Decode(ctx.Body, &config)
|
err = mapstructure.Decode(body, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
@@ -208,24 +203,7 @@ func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type GlobalConfig struct {
|
func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) {
|
||||||
CPU struct {
|
|
||||||
Whitelist bool
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserConfig struct {
|
|
||||||
CPU struct {
|
|
||||||
Global []CPUConfig
|
|
||||||
Nodes map[string][]CPUConfig
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type CPUConfig struct {
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) {
|
|
||||||
cputypes := common.Select{
|
cputypes := common.Select{
|
||||||
ID: "proctype",
|
ID: "proctype",
|
||||||
Required: true,
|
Required: true,
|
||||||
@@ -238,10 +216,10 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) {
|
|||||||
"PVEAuthCookie": auth.Token,
|
"PVEAuthCookie": auth.Token,
|
||||||
"CSRFPreventionToken": auth.CSRF,
|
"CSRFPreventionToken": auth.CSRF,
|
||||||
},
|
},
|
||||||
Body: map[string]any{},
|
|
||||||
}
|
}
|
||||||
|
body := map[string]any{}
|
||||||
path := "/global/config/resources"
|
path := "/global/config/resources"
|
||||||
res, code, err := common.RequestGetAPI(path, ctx)
|
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
@@ -249,23 +227,23 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) {
|
|||||||
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||||
}
|
}
|
||||||
global := GlobalConfig{}
|
global := GlobalConfig{}
|
||||||
err = mapstructure.Decode(ctx.Body["resources"], &global)
|
err = mapstructure.Decode(body["resources"], &global)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// get user resource config
|
// get user resource config
|
||||||
ctx.Body = map[string]any{}
|
body = map[string]any{}
|
||||||
path = "/user/config/resources"
|
path = "/user/config/resources"
|
||||||
res, code, err = common.RequestGetAPI(path, ctx)
|
res, code, err = common.RequestGetAPI(path, ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||||
}
|
}
|
||||||
user := UserConfig{}
|
user := UserConfigResources{}
|
||||||
err = mapstructure.Decode(ctx.Body, &user)
|
err = mapstructure.Decode(body, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
@@ -287,9 +265,9 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) {
|
|||||||
}
|
}
|
||||||
} else { // cpu is a blacklist
|
} else { // cpu is a blacklist
|
||||||
// get the supported cpu types from the node
|
// get the supported cpu types from the node
|
||||||
ctx.Body = map[string]any{}
|
body = map[string]any{}
|
||||||
path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.Node)
|
path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.Node)
|
||||||
res, code, err = common.RequestGetAPI(path, ctx)
|
res, code, err = common.RequestGetAPI(path, ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
@@ -299,7 +277,7 @@ func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) {
|
|||||||
supported := struct {
|
supported := struct {
|
||||||
data []CPUConfig
|
data []CPUConfig
|
||||||
}{}
|
}{}
|
||||||
err = mapstructure.Decode(ctx.Body, supported)
|
err = mapstructure.Decode(body, supported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
|
@@ -4,46 +4,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-viper/mapstructure/v2"
|
"github.com/go-viper/mapstructure/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleGETIndex(c *gin.Context) {
|
|
||||||
auth, err := common.GetAuth(c)
|
|
||||||
if err == nil { // user should be authed, try to return index with population
|
|
||||||
instances, _, err := GetClusterResources(auth)
|
|
||||||
if err != nil {
|
|
||||||
common.HandleNonFatalError(c, err)
|
|
||||||
}
|
|
||||||
c.HTML(http.StatusOK, "html/index.html", gin.H{
|
|
||||||
"global": common.Global,
|
|
||||||
"page": "index",
|
|
||||||
"instances": instances,
|
|
||||||
})
|
|
||||||
} else { // return index without populating
|
|
||||||
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleGETInstancesFragment(c *gin.Context) {
|
|
||||||
Auth, err := common.GetAuth(c)
|
|
||||||
if err == nil { // user should be authed, try to return index with population
|
|
||||||
instances, _, err := GetClusterResources(Auth)
|
|
||||||
if err != nil {
|
|
||||||
common.HandleNonFatalError(c, err)
|
|
||||||
}
|
|
||||||
c.Header("Content-Type", "text/plain")
|
|
||||||
common.TMPL.ExecuteTemplate(c.Writer, "html/index-instances.frag", gin.H{
|
|
||||||
"instances": instances,
|
|
||||||
})
|
|
||||||
c.Status(http.StatusOK)
|
|
||||||
} else { // return 401
|
|
||||||
c.Status(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// used in constructing instance cards in index
|
// used in constructing instance cards in index
|
||||||
type Node struct {
|
type Node struct {
|
||||||
Node string `json:"node"`
|
Node string `json:"node"`
|
||||||
@@ -52,12 +18,66 @@ type Node struct {
|
|||||||
|
|
||||||
// used in constructing instance cards in index
|
// used in constructing instance cards in index
|
||||||
type InstanceCard struct {
|
type InstanceCard struct {
|
||||||
VMID uint
|
VMID uint
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
Status string
|
Status string
|
||||||
Node string
|
Node string
|
||||||
NodeStatus string
|
NodeStatus string
|
||||||
|
ConfigPath string
|
||||||
|
ConsolePath string
|
||||||
|
BackupsPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// used in retriving cluster tasks
|
||||||
|
type Task struct {
|
||||||
|
Type string
|
||||||
|
Node string
|
||||||
|
User string
|
||||||
|
ID string
|
||||||
|
VMID uint
|
||||||
|
Status string
|
||||||
|
EndTime uint
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstanceStatus struct {
|
||||||
|
Status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGETIndex(c *gin.Context) {
|
||||||
|
auth, err := common.GetAuth(c)
|
||||||
|
if err == nil { // user should be authed, try to return index with population
|
||||||
|
instances, _, err := GetClusterResources(auth)
|
||||||
|
if err != nil {
|
||||||
|
common.HandleNonFatalError(c, err)
|
||||||
|
}
|
||||||
|
page := gin.H{
|
||||||
|
"global": common.Global,
|
||||||
|
"page": "index",
|
||||||
|
"instances": instances,
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "html/index.html", page)
|
||||||
|
} else { // return index without populating
|
||||||
|
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleGETInstancesFragment(c *gin.Context) {
|
||||||
|
auth, err := common.GetAuth(c)
|
||||||
|
if err == nil { // user should be authed, try to return index with population
|
||||||
|
instances, _, err := GetClusterResources(auth)
|
||||||
|
if err != nil {
|
||||||
|
common.HandleNonFatalError(c, err)
|
||||||
|
}
|
||||||
|
c.Header("Content-Type", "text/plain")
|
||||||
|
common.TMPL.ExecuteTemplate(c.Writer, "html/index-instances.go.tmpl", gin.H{
|
||||||
|
"instances": instances,
|
||||||
|
})
|
||||||
|
c.Status(http.StatusOK)
|
||||||
|
} else { // return 401
|
||||||
|
c.Status(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) {
|
func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) {
|
||||||
@@ -66,21 +86,21 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
|
|||||||
"PVEAuthCookie": auth.Token,
|
"PVEAuthCookie": auth.Token,
|
||||||
"CSRFPreventionToken": auth.CSRF,
|
"CSRFPreventionToken": auth.CSRF,
|
||||||
},
|
},
|
||||||
Body: map[string]any{},
|
|
||||||
}
|
}
|
||||||
res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", ctx)
|
body := map[string]any{}
|
||||||
|
res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if code != 200 { // if we did not successfully retrieve resources, then return 500 because auth was 1 but was invalid somehow
|
if code != 200 { // if we did not successfully retrieve resources, then return 500 because auth was 1 but was invalid somehow
|
||||||
return nil, nil, fmt.Errorf("request to /cluster/resources/ resulted in %+v", res)
|
return nil, nil, fmt.Errorf("request to /cluster/resources resulted in %+v", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
instances := map[uint]InstanceCard{}
|
instances := map[uint]InstanceCard{}
|
||||||
nodes := map[string]Node{}
|
nodes := map[string]Node{}
|
||||||
|
|
||||||
// if we successfully retrieved the resources, then process it and return index
|
// if we successfully retrieved the resources, then process it and return index
|
||||||
for _, v := range ctx.Body["data"].([]any) {
|
for _, v := range body["data"].([]any) {
|
||||||
m := v.(map[string]any)
|
m := v.(map[string]any)
|
||||||
if m["type"] == "node" {
|
if m["type"] == "node" {
|
||||||
node := Node{}
|
node := Node{}
|
||||||
@@ -101,7 +121,83 @@ func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]No
|
|||||||
for vmid, instance := range instances {
|
for vmid, instance := range instances {
|
||||||
nodestatus := nodes[instance.Node].Status
|
nodestatus := nodes[instance.Node].Status
|
||||||
instance.NodeStatus = nodestatus
|
instance.NodeStatus = nodestatus
|
||||||
|
instance.ConfigPath = fmt.Sprintf("config?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
|
||||||
|
if instance.Type == "qemu" {
|
||||||
|
instance.ConsolePath = fmt.Sprintf("%s/?console=kvm&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&novnc=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
|
||||||
|
} else if instance.Type == "lxc" {
|
||||||
|
instance.ConsolePath = fmt.Sprintf("%s/?console=lxc&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&xtermjs=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
|
||||||
|
}
|
||||||
|
instance.BackupsPath = fmt.Sprintf("backups?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
|
||||||
instances[vmid] = instance
|
instances[vmid] = instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body = map[string]any{}
|
||||||
|
res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", ctx, &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if code != 200 { // if we did not successfully retrieve tasks, then return 500 because auth was 1 but was invalid somehow
|
||||||
|
return nil, nil, fmt.Errorf("request to /cluster/tasks resulted in %+v", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
most_recent_task := map[uint]uint{}
|
||||||
|
expected_state := map[uint]string{}
|
||||||
|
|
||||||
|
for _, v := range body["data"].([]any) {
|
||||||
|
task := Task{}
|
||||||
|
err := mapstructure.Decode(v, &task)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
x, err := strconv.Atoi(task.ID)
|
||||||
|
task.VMID = uint(x)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.User != auth.Username { // task was not made by user (ie was not a power on/off task)
|
||||||
|
continue
|
||||||
|
} else if _, ok := instances[task.VMID]; !ok { // task does not refer to an instance in user's instances
|
||||||
|
continue
|
||||||
|
} else if instances[task.VMID].Node != task.Node { // task does not have the correct node reference (should not happen)
|
||||||
|
continue
|
||||||
|
} else if !(task.Type == "qmstart" || task.Type == "qmstop" || task.Type == "vzstart" || task.Type == "vzstop") { // task is not start/stop for qemu or lxc
|
||||||
|
continue
|
||||||
|
} else if !(task.Status == "running" || task.Status == "OK") { // task is not running or finished with status OK
|
||||||
|
continue
|
||||||
|
} else { // recent task is a start or stop task for user instance which is running or "OK"
|
||||||
|
if task.EndTime > most_recent_task[task.VMID] { // if the task's end time is later than the most recent one encountered
|
||||||
|
most_recent_task[task.VMID] = task.EndTime // update the most recent task
|
||||||
|
if task.Type == "qmstart" || task.Type == "vzstart" { // if the task was a start task, update the expected state to running
|
||||||
|
expected_state[task.VMID] = "running"
|
||||||
|
} else if task.Type == "qmstop" || task.Type == "vzstop" { // if the task was a stop task, update the expected state to stopped
|
||||||
|
expected_state[task.VMID] = "stopped"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for vmid, expected_state := range expected_state { // for the expected states from recent tasks
|
||||||
|
if instances[vmid].Status != expected_state { // if the current node's state from /cluster/resources differs from expected state
|
||||||
|
// get /status/current which is updated faster than /cluster/resources
|
||||||
|
instance := instances[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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if code != 200 { // if we did not successfully retrieve tasks, then return 500 because auth was 1 but was invalid somehow
|
||||||
|
return nil, nil, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := InstanceStatus{}
|
||||||
|
mapstructure.Decode(body["data"], &status)
|
||||||
|
|
||||||
|
instance.Status = status.Status
|
||||||
|
instances[vmid] = instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return instances, nodes, nil
|
return instances, nodes, nil
|
||||||
}
|
}
|
||||||
|
@@ -9,34 +9,6 @@ import (
|
|||||||
"github.com/go-viper/mapstructure/v2"
|
"github.com/go-viper/mapstructure/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetLoginRealms() ([]Realm, error) {
|
|
||||||
realms := []Realm{}
|
|
||||||
|
|
||||||
ctx := common.RequestContext{
|
|
||||||
Cookies: nil,
|
|
||||||
Body: map[string]any{},
|
|
||||||
}
|
|
||||||
res, code, err := common.RequestGetAPI("/proxmox/access/domains", ctx)
|
|
||||||
if err != nil {
|
|
||||||
return realms, err
|
|
||||||
}
|
|
||||||
if code != 200 { // we expect /access/domains to always be avaliable
|
|
||||||
return realms, fmt.Errorf("request to /proxmox/access/domains resulted in %+v", res)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range ctx.Body["data"].([]any) {
|
|
||||||
v = v.(map[string]any)
|
|
||||||
realm := Realm{}
|
|
||||||
err := mapstructure.Decode(v, &realm)
|
|
||||||
if err != nil {
|
|
||||||
return realms, err
|
|
||||||
}
|
|
||||||
realms = append(realms, realm)
|
|
||||||
}
|
|
||||||
|
|
||||||
return realms, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// used when requesting GET /access/domains
|
// used when requesting GET /access/domains
|
||||||
type GetRealmsBody struct {
|
type GetRealmsBody struct {
|
||||||
Data []Realm `json:"data"`
|
Data []Realm `json:"data"`
|
||||||
@@ -49,6 +21,35 @@ type Realm struct {
|
|||||||
Comment string `json:"comment"`
|
Comment string `json:"comment"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLoginRealms() ([]Realm, error) {
|
||||||
|
realms := []Realm{}
|
||||||
|
|
||||||
|
ctx := common.RequestContext{
|
||||||
|
Cookies: nil,
|
||||||
|
//Body: map[string]any{},
|
||||||
|
}
|
||||||
|
body := map[string]any{}
|
||||||
|
res, code, err := common.RequestGetAPI("/proxmox/access/domains", ctx, &body)
|
||||||
|
if err != nil {
|
||||||
|
return realms, err
|
||||||
|
}
|
||||||
|
if code != 200 { // we expect /access/domains to always be avaliable
|
||||||
|
return realms, fmt.Errorf("request to /proxmox/access/domains resulted in %+v", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range body["data"].([]any) {
|
||||||
|
v = v.(map[string]any)
|
||||||
|
realm := Realm{}
|
||||||
|
err := mapstructure.Decode(v, &realm)
|
||||||
|
if err != nil {
|
||||||
|
return realms, err
|
||||||
|
}
|
||||||
|
realms = append(realms, realm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return realms, nil
|
||||||
|
}
|
||||||
|
|
||||||
func HandleGETLogin(c *gin.Context) {
|
func HandleGETLogin(c *gin.Context) {
|
||||||
realms, err := GetLoginRealms()
|
realms, err := GetLoginRealms()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
55
go.mod
@@ -1,51 +1,56 @@
|
|||||||
module proxmoxaas-dashboard
|
module proxmoxaas-dashboard
|
||||||
|
|
||||||
go 1.24
|
go 1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/tdewolff/minify v2.3.6+incompatible
|
github.com/go-viper/mapstructure/v2 v2.4.0
|
||||||
|
github.com/tdewolff/minify/v2 v2.24.3
|
||||||
proxmoxaas-fabric v0.0.0
|
proxmoxaas-fabric v0.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
replace proxmoxaas-fabric => ../ProxmoxAAS-Fabric
|
replace proxmoxaas-fabric => ./ProxmoxAAS-Fabric
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/buger/goterm v1.0.4 // indirect
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/diskfs/go-diskfs v1.5.2 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/diskfs/go-diskfs v1.7.0 // indirect
|
||||||
github.com/djherbis/times v1.6.0 // indirect
|
github.com/djherbis/times v1.6.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.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/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/jinzhu/copier v0.4.0 // indirect
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/luthermonson/go-proxmox v0.2.2 // indirect
|
github.com/luthermonson/go-proxmox v0.2.3 // indirect
|
||||||
github.com/magefile/mage v1.15.0 // indirect
|
github.com/magefile/mage v1.15.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/tdewolff/minify/v2 v2.23.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/tdewolff/parse v2.3.4+incompatible // indirect
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.7.23 // indirect
|
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
||||||
github.com/tdewolff/test v1.0.11 // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||||
golang.org/x/arch v0.16.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
golang.org/x/crypto v0.37.0 // indirect
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
golang.org/x/net v0.39.0 // indirect
|
golang.org/x/crypto v0.43.0 // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/mod v0.29.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/net v0.46.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
|
golang.org/x/text v0.30.0 // indirect
|
||||||
|
golang.org/x/tools v0.38.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
)
|
)
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "proxmoxaas-dashboard",
|
"name": "proxmoxaas-dashboard",
|
||||||
"version": "0.0.1",
|
"version": "1.0.0",
|
||||||
"description": "Front-end for ProxmoxAAS",
|
"description": "Front-end for ProxmoxAAS",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
app "proxmoxaas-dashboard/app"
|
app "proxmoxaas-dashboard/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app.Run()
|
configPath := flag.String("config", "config.json", "path to config.json file")
|
||||||
|
flag.Parse()
|
||||||
|
app.Run(configPath)
|
||||||
}
|
}
|
||||||
|
@@ -76,7 +76,6 @@ input[type="radio"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
max-width: calc(min(50%, 80ch));
|
max-width: calc(min(100% - 16px, 80ch));
|
||||||
background-color: var(--main-bg-color);
|
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
}
|
}
|
@@ -100,7 +100,7 @@ img, svg {
|
|||||||
color: var(--main-text-color)
|
color: var(--main-text-color)
|
||||||
}
|
}
|
||||||
|
|
||||||
hr, * {
|
hr {
|
||||||
border-color: var(--main-text-color);
|
border-color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +114,12 @@ hr, * {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column-reverse {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
row-gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.wrap {
|
.wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
row-gap: 10px;
|
row-gap: 10px;
|
||||||
@@ -156,18 +162,26 @@ hr, * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* add hide large class similar to w3-hide-medium and w3-hide-small */
|
/* add hide large class similar to w3-hide-medium and w3-hide-small */
|
||||||
@media (width >=993px) {
|
@media screen and (width >=993px) {
|
||||||
.w3-hide-large {
|
.hide-large {display: none !important;}
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fix edge case in w3-hide-medium where width between 992 and 993 */
|
/* fixes edge case in w3-hide-medium where width between 992 and 993 */
|
||||||
@media (width <=993px) and (width >=601px){
|
@media screen and (width <=993px) and (width >=601px){
|
||||||
.w3-hide-medium{display:none!important}
|
.hide-large {display: none !important;}
|
||||||
|
.hide-medium {display:none !important}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fix edge case in w3-hide-small when width between 600 and 601 */
|
/* fixes edge case in w3-hide-small when width between 600 and 601 */
|
||||||
@media (width <=601px) {
|
@media screen and (width <=601px) {
|
||||||
.w3-hide-small{display:none!important}
|
.hide-large {display: none !important;}
|
||||||
|
.hide-medium {display:none !important}
|
||||||
|
.hide-small {display:none !important}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (width <= 440px) {
|
||||||
|
.hide-large {display: none !important;}
|
||||||
|
.hide-medium {display:none !important}
|
||||||
|
.hide-small {display:none !important}
|
||||||
|
.hide-tiny { display: none !important;}
|
||||||
}
|
}
|
@@ -7,7 +7,7 @@
|
|||||||
<link rel="modulepreload" href="scripts/dialog.js">
|
<link rel="modulepreload" href="scripts/dialog.js">
|
||||||
<style>
|
<style>
|
||||||
@media screen and (width >= 1264px){
|
@media screen and (width >= 1264px){
|
||||||
#resource-container {
|
.resource-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, calc(100% / 6));
|
grid-template-columns: repeat(auto-fill, calc(100% / 6));
|
||||||
grid-gap: 0;
|
grid-gap: 0;
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (width <= 1264px) and (width >= 680px) {
|
@media screen and (width <= 1264px) and (width >= 680px) {
|
||||||
#resource-container {
|
.resource-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, 200px);
|
grid-template-columns: repeat(auto-fill, 200px);
|
||||||
grid-gap: 0;
|
grid-gap: 0;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (width <= 680px) {
|
@media screen and (width <= 680px) {
|
||||||
#resource-container {
|
.resource-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
@@ -54,24 +54,53 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="w3-card w3-padding">
|
<section class="w3-card w3-padding">
|
||||||
<h3>Cluster Resources</h3>
|
<h3>Cluster Resources</h3>
|
||||||
<div id="resource-container">
|
<div>
|
||||||
{{range .account.Resources}}
|
{{range $category, $v := .account.Resources}}
|
||||||
{{if .Display}}
|
{{if ne $category ""}}
|
||||||
{{if eq .Type "numeric"}}
|
<h4>{{$category}}</h4>
|
||||||
{{template "resource-chart" .}}
|
{{end}}
|
||||||
{{end}}
|
<div class="resource-container">
|
||||||
{{if eq .Type "storage"}}
|
{{range $v}}
|
||||||
{{template "resource-chart" .}}
|
{{if .Display}}
|
||||||
{{end}}
|
{{if eq .Type "numeric"}}
|
||||||
{{if eq .Type "list"}}
|
|
||||||
{{range .Resources}}
|
|
||||||
{{template "resource-chart" .}}
|
{{template "resource-chart" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if eq .Type "storage"}}
|
||||||
|
{{template "resource-chart" .}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Type "list"}}
|
||||||
|
{{range .Resources}}
|
||||||
|
{{template "resource-chart" .}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</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;">
|
||||||
|
Change Password
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="new-password">New Password</label>
|
||||||
|
<input class="w3-input w3-border" id="new-password" name="new-password" type="password" required>
|
||||||
|
<label for="confirm-password">Confirm Password</label>
|
||||||
|
<input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" required>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
3
web/html/backups-backups.go.tmpl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{{range $i, $x := .backups}}
|
||||||
|
{{template "backup-card" $x}}
|
||||||
|
{{end}}
|
38
web/html/backups.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{{template "head" .}}
|
||||||
|
<script src="scripts/backups.js" type="module"></script>
|
||||||
|
<link rel="modulepreload" href="scripts/utils.js">
|
||||||
|
<link rel="modulepreload" href="scripts/dialog.js">
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
{{template "header" .}}
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<h2><a href="index">Instances</a> / {{.config.Name}} / Backups</h2>
|
||||||
|
<section class="w3-card w3-padding">
|
||||||
|
<div class="w3-row" style="border-bottom: 1px solid;">
|
||||||
|
<p class="w3-col l2 m4 s8">Time</p>
|
||||||
|
<p class="w3-col l6 m6 hide-small">Notes</p>
|
||||||
|
<p class="w3-col l2 hide-medium">Size</p>
|
||||||
|
<p class="w3-col l2 m2 s4">Actions</p>
|
||||||
|
</div>
|
||||||
|
<div id="backups-container">
|
||||||
|
{{range $i, $x := .backups}}
|
||||||
|
{{template "backup-card" $x}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="w3-container w3-center">
|
||||||
|
{{template "backups-add-backup" .}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<div class="w3-container w3-center">
|
||||||
|
<a class="w3-button w3-margin" id="exit" href="index">EXIT</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
@@ -1 +0,0 @@
|
|||||||
{{template "volumes" .config.Volumes}}
|
|
1
web/html/config-volumes.go.tmpl
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{{template "volumes" Map "Volumes" .config.Volumes "InstanceType" .config.Type}}
|
@@ -25,8 +25,8 @@
|
|||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<h2><a href="index">Instances</a> / {{.config.Name}}</h2>
|
<h2><a href="index">Instances</a> / {{.config.Name}} / Config</h2>
|
||||||
<form>
|
<form id="config-form">
|
||||||
<fieldset class="w3-card w3-padding">
|
<fieldset class="w3-card w3-padding">
|
||||||
<legend>Resources</legend>
|
<legend>Resources</legend>
|
||||||
<div class="input-grid" id="resources" style="grid-template-columns: auto auto auto 1fr;">
|
<div class="input-grid" id="resources" style="grid-template-columns: auto auto auto 1fr;">
|
||||||
@@ -43,18 +43,14 @@
|
|||||||
<fieldset class="w3-card w3-padding">
|
<fieldset class="w3-card w3-padding">
|
||||||
<legend>Volumes</legend>
|
<legend>Volumes</legend>
|
||||||
<div class="input-grid" id="volumes" style="grid-template-columns: auto auto 1fr auto;">
|
<div class="input-grid" id="volumes" style="grid-template-columns: auto auto 1fr auto;">
|
||||||
{{template "volumes" .config.Volumes}}
|
{{template "volumes" Map "Volumes" .config.Volumes "InstanceType" .config.Type}}
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-container w3-center">
|
<div class="w3-container w3-center">
|
||||||
<button type="button" id="disk-add" class="w3-button" aria-label="Add New Disk">
|
<!--Add Disk Button & Dialog Template-->
|
||||||
<span class="large" style="margin: 0;">Add Disk</span>
|
{{template "volumes-add-disk" .}}
|
||||||
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Disk"><use href="images/actions/disk/add-disk.svg#symb"></use></svg>
|
<!--Add CD Button & Dialog Template-->
|
||||||
</button>
|
|
||||||
{{if eq .config.Type "VM"}}
|
{{if eq .config.Type "VM"}}
|
||||||
<button type="button" id="cd-add" class="w3-button" aria-label="Add New CD">
|
{{template "volumes-add-cd"}}
|
||||||
<span class="large" style="margin: 0;">Mount CD</span>
|
|
||||||
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New CDROM"><use href="images/actions/disk/add-cd.svg#symb"></use></svg>
|
|
||||||
</button>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -64,10 +60,8 @@
|
|||||||
{{template "nets" .config.Nets}}
|
{{template "nets" .config.Nets}}
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-container w3-center">
|
<div class="w3-container w3-center">
|
||||||
<button type="button" id="network-add" class="w3-button" aria-label="Add New Network Interface">
|
<!--Add Net Button & Dialog Template-->
|
||||||
<span class="large" style="margin: 0;">Add Network</span>
|
{{template "nets-add-net"}}
|
||||||
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Network Interface"><use href="images/actions/network/add.svg#symb"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{{if eq .config.Type "VM"}}
|
{{if eq .config.Type "VM"}}
|
||||||
@@ -77,10 +71,8 @@
|
|||||||
{{template "devices" .config.Devices}}
|
{{template "devices" .config.Devices}}
|
||||||
</div>
|
</div>
|
||||||
<div class="w3-container w3-center">
|
<div class="w3-container w3-center">
|
||||||
<button type="button" id="device-add" class="w3-button" aria-label="Add New PCIe Device">
|
<!--Add Device Button & Dialog Template-->
|
||||||
<span class="large" style="margin: 0;">Add Device</span>
|
{{template "devices-add-device"}}
|
||||||
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New PCIe Device"><use href="images/actions/device/add.svg#symb"></use></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="w3-card w3-padding">
|
<fieldset class="w3-card w3-padding">
|
||||||
@@ -91,7 +83,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="w3-container w3-center" id="form-actions">
|
<div class="w3-container w3-center" id="form-actions">
|
||||||
<button class="w3-button w3-margin" id="exit" type="button">EXIT</button>
|
<button class="w3-button w3-margin" id="exit" type="submit">EXIT</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -8,12 +8,6 @@
|
|||||||
<link rel="modulepreload" href="scripts/dialog.js">
|
<link rel="modulepreload" href="scripts/dialog.js">
|
||||||
<link rel="modulepreload" href="scripts/clientsync.js">
|
<link rel="modulepreload" href="scripts/clientsync.js">
|
||||||
<style>
|
<style>
|
||||||
#instance-container > div {
|
|
||||||
border-bottom: 1px solid white;
|
|
||||||
}
|
|
||||||
#instance-container > div:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
@media screen and (width >= 440px) {
|
@media screen and (width >= 440px) {
|
||||||
#vm-search {
|
#vm-search {
|
||||||
max-width: calc(100% - 10px - 152px);
|
max-width: calc(100% - 10px - 152px);
|
||||||
@@ -24,6 +18,50 @@
|
|||||||
max-width: calc(100% - 10px - 47px);
|
max-width: calc(100% - 10px - 47px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media screen and (width >= 993px) {
|
||||||
|
#instance-table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, auto);
|
||||||
|
grid-column-gap: 1em;
|
||||||
|
grid-row-gap: 0.25em;
|
||||||
|
}
|
||||||
|
#instance-table-header, #instance-container, #instance-table instance-card {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (width <= 993px) and (width >= 601px){
|
||||||
|
#instance-table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, auto);
|
||||||
|
grid-column-gap: 1em;
|
||||||
|
grid-row-gap: 0.25em;
|
||||||
|
}
|
||||||
|
#instance-table-header, #instance-container, #instance-table instance-card {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (width <= 601px) and (width >= 440px){
|
||||||
|
#instance-table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, auto);
|
||||||
|
grid-column-gap: 1em;
|
||||||
|
grid-row-gap: 0.25em;
|
||||||
|
}
|
||||||
|
#instance-table-header, #instance-container, #instance-table instance-card {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (width <= 440px) {
|
||||||
|
#instance-table {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, auto);
|
||||||
|
grid-column-gap: 1em;
|
||||||
|
grid-row-gap: 0.25em;
|
||||||
|
}
|
||||||
|
#instance-table-header, #instance-container, #instance-table instance-card {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -35,25 +73,72 @@
|
|||||||
<h2>Instances</h2>
|
<h2>Instances</h2>
|
||||||
<div class="w3-card w3-padding">
|
<div class="w3-card w3-padding">
|
||||||
<div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;">
|
<div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;">
|
||||||
<form id="vm-search" role="search" class="flex row nowrap">
|
<form id="vm-search" role="search" class="flex row nowrap" tabindex="0">
|
||||||
<svg role="img" aria-label="Search Instances"><use href="images/common/search.svg#symb"></use></svg>
|
<svg role="img" aria-label="Search Instances"><use href="images/common/search.svg#symb"></use></svg>
|
||||||
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
|
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
|
||||||
</form>
|
</form>
|
||||||
|
<!--Add Instance Button & Dialog Template-->
|
||||||
<button type="button" id="instance-add" class="w3-button" aria-label="create new instance">
|
<button type="button" id="instance-add" class="w3-button" aria-label="create new instance">
|
||||||
<span class="large" style="margin: 0;">Create Instance</span>
|
<span class="large" style="margin: 0;">Create Instance</span>
|
||||||
<svg class="small" style="height: 1lh; width: 1lh;" role="img" aria-label="Create New Instance"><use href="images/actions/instance/add.svg#symb"></use></svg>
|
<svg class="small" style="height: 1lh; width: 1lh;" role="img" aria-label="Create New Instance"><use href="images/actions/instance/add.svg#symb"></use></svg>
|
||||||
</button>
|
</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;">
|
||||||
|
Create New Instance
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="type">Instance Type</label>
|
||||||
|
<select class="w3-select w3-border" name="type" id="type" selected-index="-1" required>
|
||||||
|
<option value="lxc">Container</option>
|
||||||
|
<option value="qemu">Virtual Machine</option>
|
||||||
|
</select>
|
||||||
|
<label for="node">Node</label>
|
||||||
|
<select class="w3-select w3-border" name="node" id="node" required></select>
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input class="w3-input w3-border" name="name" id="name" 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>
|
||||||
|
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16" step="1" required>
|
||||||
|
<p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p>
|
||||||
|
<label class="container-specific none" for="swap">Swap (MiB)</label>
|
||||||
|
<input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled>
|
||||||
|
<label class="container-specific none" for="template-image">Template Image</label>
|
||||||
|
<select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select>
|
||||||
|
<label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label>
|
||||||
|
<select class="w3-select w3-border container-specific none" name="rootfs-storage" id="rootfs-storage" required disabled></select>
|
||||||
|
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
|
||||||
|
<input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled>
|
||||||
|
<label class="container-specific none" for="password">Password</label>
|
||||||
|
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled>
|
||||||
|
<label class="container-specific none" for="confirm-password">Confirm Password</label>
|
||||||
|
<input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" required disabled>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div id="instance-table">
|
||||||
<div class="w3-row w3-hide-small" style="border-bottom: 1px solid;">
|
<div id="instance-table-header">
|
||||||
<p class="w3-col l1 m2 w3-hide-small">ID</p>
|
<p>ID</p>
|
||||||
<p class="w3-col l2 m3 w3-hide-small">Name</p>
|
<p>Name</p>
|
||||||
<p class="w3-col l1 m2 w3-hide-small">Type</p>
|
<p class="hide-tiny">Type</p>
|
||||||
<p class="w3-col l2 m3 w3-hide-small">Status</p>
|
<p class="hide-small">Status</p>
|
||||||
<p class="w3-col l2 w3-hide-medium w3-hide-small">Host Name</p>
|
<p class="hide-medium">Host Name</p>
|
||||||
<p class="w3-col l2 w3-hide-medium w3-hide-small">Host Status</p>
|
<p class="hide-medium">Host Status</p>
|
||||||
<p class="w3-col l2 m2 w3-hide-small">Actions</p>
|
<p>Actions</p>
|
||||||
</div>
|
</div>
|
||||||
|
<hr style="grid-column: 1 / -1; padding: 0; margin: 0;">
|
||||||
<div id="instance-container">
|
<div id="instance-container">
|
||||||
{{range .instances}}
|
{{range .instances}}
|
||||||
{{template "instance-card" .}}
|
{{template "instance-card" .}}
|
||||||
|
1
web/images/actions/backups/config.svg
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../common/config.svg
|
1
web/images/actions/backups/delete-active.svg
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../common/delete-active.svg
|
1
web/images/actions/backups/restore.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="symb" aria-label="config" viewBox="4 2 17 19" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M10 8H5V3m.291 13.357a8 8 0 10.188-8.991" stroke="currentColor" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
After Width: | Height: | Size: 332 B |
1
web/images/actions/instance/backup-active.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="symb" role="img" aria-label="backup" viewBox="1 1 22 22" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M18.172 1a2 2 0 011.414.586l2.828 2.828A2 2 0 0123 5.828V20a3 3 0 01-3 3H4a3 3 0 01-3-3V4a3 3 0 013-3h14.172zM4 3a1 1 0 00-1 1v16a1 1 0 001 1h1v-6a3 3 0 013-3h8a3 3 0 013 3v6h1a1 1 0 001-1V6.828a2 2 0 00-.586-1.414l-1.828-1.828A2 2 0 0017.172 3H17v2a3 3 0 01-3 3h-4a3 3 0 01-3-3V3H4zm13 18v-6a1 1 0 00-1-1H8a1 1 0 00-1 1v6h10zM9 3h6v2a1 1 0 01-1 1h-4a1 1 0 01-1-1V3z" fill="currentColor"/></svg>
|
After Width: | Height: | Size: 631 B |
1
web/images/actions/instance/backup-inactive.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="symb" role="img" aria-label="backup" viewBox="1 1 22 22" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M18.172 1a2 2 0 011.414.586l2.828 2.828A2 2 0 0123 5.828V20a3 3 0 01-3 3H4a3 3 0 01-3-3V4a3 3 0 013-3h14.172zM4 3a1 1 0 00-1 1v16a1 1 0 001 1h1v-6a3 3 0 013-3h8a3 3 0 013 3v6h1a1 1 0 001-1V6.828a2 2 0 00-.586-1.414l-1.828-1.828A2 2 0 0017.172 3H17v2a3 3 0 01-3 3h-4a3 3 0 01-3-3V3H4zm13 18v-6a1 1 0 00-1-1H8a1 1 0 00-1 1v6h10zM9 3h6v2a1 1 0 01-1 1h-4a1 1 0 01-1-1V3z" fill="#808080"/></svg>
|
After Width: | Height: | Size: 540 B |
@@ -1 +1 @@
|
|||||||
<svg id="symb" role="img" aria-label="instance console" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="currentColor"/></svg>
|
<svg id="symb" role="img" aria-label="instance console" viewBox="2 2 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="currentColor"/></svg>
|
Before Width: | Height: | Size: 565 B After Width: | Height: | Size: 565 B |
@@ -1 +1 @@
|
|||||||
<svg id="symb" role="img" aria-label="" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="#808080"/></svg>
|
<svg id="symb" role="img" aria-label="" viewBox="2 2 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="#808080"/></svg>
|
Before Width: | Height: | Size: 458 B After Width: | Height: | Size: 458 B |
@@ -1 +1 @@
|
|||||||
<svg id="symb" role="img" aria-label="start instance" xmlns="http://www.w3.org/2000/svg" viewBox="2.8 2.4 12 12"><path d="M4.25 3l1.166-.624 8 5.333v1.248l-8 5.334-1.166-.624V3zm1.5 1.401v7.864l5.898-3.932L5.75 4.401z" fill="#0f0"/></svg>
|
<svg id="symb" role="img" aria-label="start instance" xmlns="http://www.w3.org/2000/svg" viewBox="4.2 2.4 9 12"><path d="M4.25 3l1.166-.624 8 5.333v1.248l-8 5.334-1.166-.624V3zm1.5 1.401v7.864l5.898-3.932L5.75 4.401z" fill="#0f0"/></svg>
|
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 237 B |
@@ -1 +1 @@
|
|||||||
<svg id="symb" role="img" aria-label="stop instance" xmlns="http://www.w3.org/2000/svg" viewBox="-25 -25 380 380"><path stroke-width="20" d="M315 0H15C6.716 0 0 6.716 0 15v300c0 8.284 6.716 15 15 15h300c8.284 0 15-6.716 15-15V15c0-8.284-6.716-15-15-15zm-15 300H30V30h270v270z" stroke="#f00" fill="#f00"/></svg>
|
<svg id="symb" role="img" aria-label="stop instance" xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 350 350"><path stroke-width="20" d="M315 0H15C6.716 0 0 6.716 0 15v300c0 8.284 6.716 15 15 15h300c8.284 0 15-6.716 15-15V15c0-8.284-6.716-15-15-15zm-15 300H30V30h270v270z" stroke="#f00" fill="#f00"/></svg>
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 310 B |
@@ -1 +1 @@
|
|||||||
<svg id="symb" role="img" aria-label="config device" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="currentColor"/></svg>
|
<svg id="symb" role="img" aria-label="config" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="currentColor"/></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
|||||||
<svg id="symb" role="img" aria-label="delete" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#f00" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg id="symb" role="img" aria-label="delete" viewBox="3 2 18 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#f00" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
Before Width: | Height: | Size: 310 B After Width: | Height: | Size: 310 B |
@@ -1 +1 @@
|
|||||||
<svg id="symb" role="img" aria-label="" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#ffbfbf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg id="symb" role="img" aria-label="" viewBox="3 2 18 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#ffbfbf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
@@ -1 +1 @@
|
|||||||
<svg id="symb" xmlns="http://www.w3.org/2000/svg"><g fill="#0f0" font-family="monospace" font-weight="bold"><text y="14" font-size="16">H</text><text x="9" y="8" font-size="10">0</text></g></svg>
|
<svg id="symb" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><g fill="#0f0" font-family="monospace" font-weight="bold"><text y="14" font-size="16">H</text><text x="9" y="8" font-size="10">0</text></g></svg>
|
Before Width: | Height: | Size: 195 B After Width: | Height: | Size: 215 B |
@@ -1,4 +1,4 @@
|
|||||||
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
|
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
|
||||||
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
|
||||||
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
|
||||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||||
@@ -108,6 +108,8 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
|
||||||
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
|
||||||
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
|
||||||
|
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
|
||||||
|
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
|
||||||
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
|
||||||
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
|
||||||
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
|
||||||
@@ -148,6 +150,7 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
.w3-button:hover{color:#000!important;background-color:#ccc!important}
|
||||||
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
|
||||||
.w3-hover-none:hover{box-shadow:none!important}
|
.w3-hover-none:hover{box-shadow:none!important}
|
||||||
|
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
|
||||||
/* Colors */
|
/* Colors */
|
||||||
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
|
||||||
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
|
||||||
@@ -175,6 +178,19 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
|
||||||
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
|
||||||
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
|
||||||
|
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
|
||||||
|
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
|
||||||
|
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
|
||||||
|
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
|
||||||
|
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
|
||||||
|
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
|
||||||
|
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
|
||||||
|
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
|
||||||
|
.w3-danger{color:#fff!important;background-color:#dd0000!important}
|
||||||
|
.w3-note{color:#000!important;background-color:#fff599!important}
|
||||||
|
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
|
||||||
|
.w3-warning{color:#000!important;background-color:#ffb305!important}
|
||||||
|
.w3-success{color:#fff!important;background-color:#008a00!important}
|
||||||
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
|
||||||
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
|
||||||
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
|
||||||
@@ -232,4 +248,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
|
|||||||
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
|
||||||
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
|
||||||
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
|
||||||
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
|
||||||
|
@@ -1,24 +1,17 @@
|
|||||||
import { dialog } from "./dialog.js";
|
|
||||||
import { requestAPI, setAppearance } from "./utils.js";
|
import { requestAPI, setAppearance } from "./utils.js";
|
||||||
|
import { dialog } from "./dialog.js";
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", init);
|
window.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
async function init () {
|
async function init () {
|
||||||
setAppearance();
|
setAppearance();
|
||||||
|
|
||||||
document.querySelector("#change-password").addEventListener("click", handlePasswordChangeForm);
|
document.querySelector("#change-password").addEventListener("click", handlePasswordChangeButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePasswordChangeForm () {
|
function handlePasswordChangeButton () {
|
||||||
const body = `
|
const template = document.querySelector("#change-password-dialog");
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
const d = dialog(template, async (result, form) => {
|
||||||
<label for="new-password">New Password</label>
|
|
||||||
<input class="w3-input w3-border" id="new-password" name="new-password" type="password"required>
|
|
||||||
<label for="confirm-password">Confirm Password</label>
|
|
||||||
<input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" required>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
const d = dialog("Change Password", body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const result = await requestAPI("/access/password", "POST", { password: form.get("new-password") });
|
const result = await requestAPI("/access/password", "POST", { password: form.get("new-password") });
|
||||||
if (result.status !== 200) {
|
if (result.status !== 200) {
|
||||||
@@ -29,11 +22,9 @@ function handlePasswordChangeForm () {
|
|||||||
|
|
||||||
const password = d.querySelector("#new-password");
|
const password = d.querySelector("#new-password");
|
||||||
const confirmPassword = d.querySelector("#confirm-password");
|
const confirmPassword = d.querySelector("#confirm-password");
|
||||||
|
|
||||||
function validatePassword () {
|
function validatePassword () {
|
||||||
confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : "");
|
confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
password.addEventListener("change", validatePassword);
|
password.addEventListener("change", validatePassword);
|
||||||
confirmPassword.addEventListener("keyup", validatePassword);
|
confirmPassword.addEventListener("keyup", validatePassword);
|
||||||
}
|
}
|
||||||
|
149
web/scripts/backups.js
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { requestAPI, getURIData, setAppearance, requestDash } from "./utils.js";
|
||||||
|
import { alert, dialog } from "./dialog.js";
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
|
let node;
|
||||||
|
let type;
|
||||||
|
let vmid;
|
||||||
|
|
||||||
|
async function init () {
|
||||||
|
setAppearance();
|
||||||
|
|
||||||
|
const uriData = getURIData();
|
||||||
|
node = uriData.node;
|
||||||
|
type = uriData.type;
|
||||||
|
vmid = uriData.vmid;
|
||||||
|
|
||||||
|
document.querySelector("#backup-add").addEventListener("click", handleBackupAddButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackupCard extends HTMLElement {
|
||||||
|
shadowRoot = null;
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
super();
|
||||||
|
const internals = this.attachInternals();
|
||||||
|
this.shadowRoot = internals.shadowRoot;
|
||||||
|
|
||||||
|
const editButton = this.shadowRoot.querySelector("#edit-btn");
|
||||||
|
if (editButton.classList.contains("clickable")) {
|
||||||
|
editButton.onclick = this.handleEditButton.bind(this);
|
||||||
|
editButton.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.editButton();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
|
||||||
|
if (deleteButton.classList.contains("clickable")) {
|
||||||
|
deleteButton.onclick = this.handleDeleteButton.bind(this);
|
||||||
|
deleteButton.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.handleDeleteButton();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreButton = this.shadowRoot.querySelector("#restore-btn");
|
||||||
|
if (restoreButton.classList.contains("clickable")) {
|
||||||
|
restoreButton.onclick = this.handleRestoreButton.bind(this);
|
||||||
|
restoreButton.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.handleRestoreButton();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get volid () {
|
||||||
|
return this.dataset.volid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleEditButton () {
|
||||||
|
const template = this.shadowRoot.querySelector("#edit-dialog");
|
||||||
|
dialog(template, async (result, form) => {
|
||||||
|
if (result === "confirm") {
|
||||||
|
const body = {
|
||||||
|
volid: this.volid,
|
||||||
|
notes: form.get("notes")
|
||||||
|
};
|
||||||
|
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup/notes`, "POST", body);
|
||||||
|
if (result.status !== 200) {
|
||||||
|
alert(`Attempted to edit backup but got: ${result.error}`);
|
||||||
|
}
|
||||||
|
refreshBackups();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDeleteButton () {
|
||||||
|
const template = this.shadowRoot.querySelector("#delete-dialog");
|
||||||
|
dialog(template, async (result, form) => {
|
||||||
|
if (result === "confirm") {
|
||||||
|
const body = {
|
||||||
|
volid: this.volid
|
||||||
|
};
|
||||||
|
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup`, "DELETE", body);
|
||||||
|
if (result.status !== 200) {
|
||||||
|
alert(`Attempted to delete backup but got: ${result.error}`);
|
||||||
|
}
|
||||||
|
refreshBackups();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRestoreButton () {
|
||||||
|
const template = this.shadowRoot.querySelector("#restore-dialog");
|
||||||
|
dialog(template, async (result, form) => {
|
||||||
|
if (result === "confirm") {
|
||||||
|
const body = {
|
||||||
|
volid: this.volid
|
||||||
|
};
|
||||||
|
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup/restore`, "POST", body);
|
||||||
|
if (result.status !== 200) {
|
||||||
|
alert(`Attempted to delete backup but got: ${result.error}`);
|
||||||
|
}
|
||||||
|
refreshBackups();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("backup-card", BackupCard);
|
||||||
|
|
||||||
|
async function getBackupsFragment () {
|
||||||
|
return await requestDash(`/backups/backups?node=${node}&type=${type}&vmid=${vmid}`, "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshBackups () {
|
||||||
|
let backups = await getBackupsFragment();
|
||||||
|
if (backups.status !== 200) {
|
||||||
|
alert("Error fetching backups.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
backups = backups.data;
|
||||||
|
const container = document.querySelector("#backups-container");
|
||||||
|
container.setHTMLUnsafe(backups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBackupAddButton () {
|
||||||
|
const template = document.querySelector("#create-backup-dialog");
|
||||||
|
dialog(template, async (result, form) => {
|
||||||
|
if (result === "confirm") {
|
||||||
|
const body = {
|
||||||
|
notes: form.get("notes")
|
||||||
|
};
|
||||||
|
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/backup`, "POST", body);
|
||||||
|
if (result.status !== 200) {
|
||||||
|
alert(`Attempted to create backup but got: ${result.error}`);
|
||||||
|
}
|
||||||
|
refreshBackups();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -19,7 +19,7 @@ async function init () {
|
|||||||
initNetworks();
|
initNetworks();
|
||||||
initDevices();
|
initDevices();
|
||||||
|
|
||||||
document.querySelector("#exit").addEventListener("click", handleFormExit);
|
document.querySelector("#config-form").addEventListener("submit", handleFormExit);
|
||||||
}
|
}
|
||||||
|
|
||||||
class VolumeAction extends HTMLElement {
|
class VolumeAction extends HTMLElement {
|
||||||
@@ -29,6 +29,7 @@ class VolumeAction extends HTMLElement {
|
|||||||
super();
|
super();
|
||||||
const internals = this.attachInternals();
|
const internals = this.attachInternals();
|
||||||
this.shadowRoot = internals.shadowRoot;
|
this.shadowRoot = internals.shadowRoot;
|
||||||
|
this.template = this.shadowRoot.querySelector("#dialog-template");
|
||||||
if (this.dataset.type === "move") {
|
if (this.dataset.type === "move") {
|
||||||
this.addEventListener("click", this.handleDiskMove);
|
this.addEventListener("click", this.handleDiskMove);
|
||||||
}
|
}
|
||||||
@@ -53,9 +54,7 @@ class VolumeAction extends HTMLElement {
|
|||||||
|
|
||||||
async handleDiskDetach () {
|
async handleDiskDetach () {
|
||||||
const disk = this.dataset.volume;
|
const disk = this.dataset.volume;
|
||||||
const header = `Detach ${disk}`;
|
dialog(this.template, async (result, form) => {
|
||||||
const body = `<p>Are you sure you want to detach disk ${disk}</p>`;
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/detach`, "POST");
|
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/detach`, "POST");
|
||||||
@@ -69,20 +68,13 @@ class VolumeAction extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDiskAttach () {
|
async handleDiskAttach () {
|
||||||
const header = `Attach ${this.dataset.volume}`;
|
dialog(this.template, async (result, form) => {
|
||||||
const body = `
|
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="device">${type === "qemu" ? "SCSI" : "MP"}</label>
|
|
||||||
<input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "30" : "255"}" required>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const device = form.get("device");
|
const device = form.get("device");
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
const body = {
|
const body = {
|
||||||
source: this.dataset.volume.replace("unused", "")
|
source: this.dataset.volume.replace("unused", ""),
|
||||||
|
mp: form.get("mp")
|
||||||
};
|
};
|
||||||
const prefix = type === "qemu" ? "scsi" : "mp";
|
const prefix = type === "qemu" ? "scsi" : "mp";
|
||||||
const disk = `${prefix}${device}`;
|
const disk = `${prefix}${device}`;
|
||||||
@@ -97,15 +89,7 @@ class VolumeAction extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDiskResize () {
|
async handleDiskResize () {
|
||||||
const header = `Resize ${this.dataset.volume}`;
|
dialog(this.template, async (result, form) => {
|
||||||
const body = `
|
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="size-increment">Size Increment (GiB)</label>
|
|
||||||
<input class="w3-input w3-border" name="size-increment" id="size-increment" type="number" min="0" max="131072">
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const disk = this.dataset.volume;
|
const disk = this.dataset.volume;
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
@@ -123,25 +107,7 @@ class VolumeAction extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDiskMove () {
|
async handleDiskMove () {
|
||||||
const content = type === "qemu" ? "images" : "rootdir";
|
const d = dialog(this.template, async (result, form) => {
|
||||||
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
|
||||||
const header = `Move ${this.dataset.volume}`;
|
|
||||||
let options = "";
|
|
||||||
storage.data.forEach((element) => {
|
|
||||||
if (element.content.includes(content)) {
|
|
||||||
options += `<option value="${element.storage}">${element.storage}</option>"`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select"><option hidden disabled selected value></option>${options}</select>`;
|
|
||||||
|
|
||||||
const body = `
|
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
${select}
|
|
||||||
<label for="delete-check">Delete Source</label><input class="w3-input w3-border" name="delete-check" id="delete-check" type="checkbox" checked required>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const disk = this.dataset.volume;
|
const disk = this.dataset.volume;
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
@@ -157,13 +123,20 @@ class VolumeAction extends HTMLElement {
|
|||||||
refreshBoot();
|
refreshBoot();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const content = type === "qemu" ? "images" : "rootdir";
|
||||||
|
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
||||||
|
const select = d.querySelector("#storage-select");
|
||||||
|
storage.data.forEach((element) => {
|
||||||
|
if (element.content.includes(content)) {
|
||||||
|
select.add(new Option(element.storage));
|
||||||
|
}
|
||||||
|
select.selectedIndex = -1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDiskDelete () {
|
async handleDiskDelete () {
|
||||||
const disk = this.dataset.volume;
|
const disk = this.dataset.volume;
|
||||||
const header = `Delete ${disk}`;
|
dialog(this.template, async (result, form) => {
|
||||||
const body = `<p>Are you sure you want to <strong>delete</strong> disk ${disk}</p>`;
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/delete`, "DELETE");
|
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/delete`, "DELETE");
|
||||||
@@ -201,26 +174,8 @@ async function refreshVolumes () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDiskAdd () {
|
async function handleDiskAdd () {
|
||||||
const content = type === "qemu" ? "images" : "rootdir";
|
const template = document.querySelector("#add-disk-dialog");
|
||||||
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
const d = dialog(template, async (result, form) => {
|
||||||
const header = "Create New Disk";
|
|
||||||
let options = "";
|
|
||||||
storage.data.forEach((element) => {
|
|
||||||
if (element.content.includes(content)) {
|
|
||||||
options += `<option value="${element.storage}">${element.storage}</option>"`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required><option hidden disabled selected value></option>${options}</select>`;
|
|
||||||
|
|
||||||
const body = `
|
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="device">${type === "qemu" ? "SCSI" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "30" : "255"}" value="0" required>
|
|
||||||
${select}
|
|
||||||
<label for="size">Size (GiB)</label><input class="w3-input w3-border" name="size" id="size" type="number" min="0" max="131072" required>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const body = {
|
const body = {
|
||||||
storage: form.get("storage-select"),
|
storage: form.get("storage-select"),
|
||||||
@@ -237,19 +192,21 @@ async function handleDiskAdd () {
|
|||||||
refreshBoot();
|
refreshBoot();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const content = type === "qemu" ? "images" : "rootdir";
|
||||||
|
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
||||||
|
const select = d.querySelector("#storage-select");
|
||||||
|
storage.data.forEach((element) => {
|
||||||
|
if (element.content.includes(content)) {
|
||||||
|
select.add(new Option(element.storage));
|
||||||
|
}
|
||||||
|
select.selectedIndex = -1;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCDAdd () {
|
async function handleCDAdd () {
|
||||||
const isos = await requestAPI("/user/vm-isos", "GET");
|
const template = document.querySelector("#add-cd-dialog");
|
||||||
const header = "Mount a CDROM";
|
const d = dialog(template, async (result, form) => {
|
||||||
const body = `
|
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="device">IDE</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="3" required>
|
|
||||||
<label for="iso-select">Image</label><select class="w3-select w3-border" name="iso-select" id="iso-select" required></select>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const d = dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const body = {
|
const body = {
|
||||||
iso: form.get("iso-select")
|
iso: form.get("iso-select")
|
||||||
@@ -264,12 +221,13 @@ async function handleCDAdd () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const isoSelect = d.querySelector("#iso-select");
|
const isos = await requestAPI("/user/vm-isos", "GET");
|
||||||
|
const select = d.querySelector("#iso-select");
|
||||||
|
|
||||||
for (const iso of isos) {
|
for (const iso of isos) {
|
||||||
isoSelect.append(new Option(iso.name, iso.volid));
|
select.add(new Option(iso.name, iso.volid));
|
||||||
}
|
}
|
||||||
isoSelect.selectedIndex = -1;
|
select.selectedIndex = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NetworkAction extends HTMLElement {
|
class NetworkAction extends HTMLElement {
|
||||||
@@ -279,6 +237,7 @@ class NetworkAction extends HTMLElement {
|
|||||||
super();
|
super();
|
||||||
const internals = this.attachInternals();
|
const internals = this.attachInternals();
|
||||||
this.shadowRoot = internals.shadowRoot;
|
this.shadowRoot = internals.shadowRoot;
|
||||||
|
this.template = this.shadowRoot.querySelector("#dialog-template");
|
||||||
if (this.dataset.type === "config") {
|
if (this.dataset.type === "config") {
|
||||||
this.addEventListener("click", this.handleNetworkConfig);
|
this.addEventListener("click", this.handleNetworkConfig);
|
||||||
}
|
}
|
||||||
@@ -293,16 +252,9 @@ class NetworkAction extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleNetworkConfig () {
|
async handleNetworkConfig () {
|
||||||
const netID = this.dataset.network;
|
|
||||||
const netDetails = this.dataset.value;
|
const netDetails = this.dataset.value;
|
||||||
const header = `Edit ${netID}`;
|
const netID = this.dataset.network;
|
||||||
const body = `
|
const d = dialog(this.template, async (result, form) => {
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const d = dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
const body = {
|
const body = {
|
||||||
@@ -323,9 +275,7 @@ class NetworkAction extends HTMLElement {
|
|||||||
|
|
||||||
async handleNetworkDelete () {
|
async handleNetworkDelete () {
|
||||||
const netID = this.dataset.network;
|
const netID = this.dataset.network;
|
||||||
const header = `Delete ${netID}`;
|
dialog(this.template, async (result, form) => {
|
||||||
const body = "";
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
setSVGSrc(document.querySelector(`svg[data-network="${netID}"]`), "images/status/loading.svg");
|
setSVGSrc(document.querySelector(`svg[data-network="${netID}"]`), "images/status/loading.svg");
|
||||||
const net = `${netID}`;
|
const net = `${netID}`;
|
||||||
@@ -361,17 +311,8 @@ async function refreshNetworks () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleNetworkAdd () {
|
async function handleNetworkAdd () {
|
||||||
const header = "Create Network Interface";
|
const template = document.querySelector("#add-net-dialog");
|
||||||
let body = `
|
dialog(template, async (result, form) => {
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="netid">Interface ID</label><input type="number" id="netid" name="netid" class="w3-input w3-border">
|
|
||||||
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
|
|
||||||
`;
|
|
||||||
if (type === "lxc") {
|
|
||||||
body += "<label for=\"name\">Interface Name</label><input type=\"text\" id=\"name\" name=\"name\" class=\"w3-input w3-border\">";
|
|
||||||
}
|
|
||||||
body += "</form>";
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const body = {
|
const body = {
|
||||||
rate: form.get("rate")
|
rate: form.get("rate")
|
||||||
@@ -398,6 +339,7 @@ class DeviceAction extends HTMLElement {
|
|||||||
super();
|
super();
|
||||||
const internals = this.attachInternals();
|
const internals = this.attachInternals();
|
||||||
this.shadowRoot = internals.shadowRoot;
|
this.shadowRoot = internals.shadowRoot;
|
||||||
|
this.template = this.shadowRoot.querySelector("#dialog-template");
|
||||||
if (this.dataset.type === "config") {
|
if (this.dataset.type === "config") {
|
||||||
this.addEventListener("click", this.handleDeviceConfig);
|
this.addEventListener("click", this.handleDeviceConfig);
|
||||||
}
|
}
|
||||||
@@ -415,14 +357,7 @@ class DeviceAction extends HTMLElement {
|
|||||||
const deviceID = this.dataset.device;
|
const deviceID = this.dataset.device;
|
||||||
const deviceDetails = this.dataset.value;
|
const deviceDetails = this.dataset.value;
|
||||||
const deviceName = this.dataset.name;
|
const deviceName = this.dataset.name;
|
||||||
const header = `Edit Expansion Card ${deviceID}`;
|
const d = dialog(this.template, async (result, form) => {
|
||||||
const body = `
|
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="device">Device</label><select id="device" name="device" required></select><label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const d = dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
const body = {
|
const body = {
|
||||||
@@ -448,9 +383,7 @@ class DeviceAction extends HTMLElement {
|
|||||||
|
|
||||||
async handleDeviceDelete () {
|
async handleDeviceDelete () {
|
||||||
const deviceID = this.dataset.device;
|
const deviceID = this.dataset.device;
|
||||||
const header = `Remove Expansion Card ${deviceID}`;
|
dialog(this.template, async (result, form) => {
|
||||||
const body = "";
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
const device = `${deviceID}`;
|
const device = `${deviceID}`;
|
||||||
@@ -487,15 +420,8 @@ async function refreshDevices () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeviceAdd () {
|
async function handleDeviceAdd () {
|
||||||
const header = "Add Expansion Card";
|
const template = document.querySelector("#add-device-dialog");
|
||||||
const body = `
|
const d = dialog(template, async (result, form) => {
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="hostpci">Device Bus</label><input type="number" id="hostpci" name="hostpci" class="w3-input w3-border">
|
|
||||||
<label for="device">Device</label><select id="device" name="device" required></select>
|
|
||||||
<label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
const d = dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const hostpci = form.get("hostpci");
|
const hostpci = form.get("hostpci");
|
||||||
const body = {
|
const body = {
|
||||||
@@ -523,14 +449,15 @@ async function refreshBoot () {
|
|||||||
if (boot.status !== 200) {
|
if (boot.status !== 200) {
|
||||||
alert("Error fetching instance boot order.");
|
alert("Error fetching instance boot order.");
|
||||||
}
|
}
|
||||||
else {
|
else if (type === "qemu") {
|
||||||
boot = boot.data;
|
boot = boot.data;
|
||||||
const order = document.querySelector("#boot-order");
|
const order = document.querySelector("#boot-order");
|
||||||
order.setHTMLUnsafe(boot);
|
order.setHTMLUnsafe(boot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFormExit () {
|
async function handleFormExit (event) {
|
||||||
|
event.preventDefault();
|
||||||
const body = {
|
const body = {
|
||||||
cores: document.querySelector("#cores").value,
|
cores: document.querySelector("#cores").value,
|
||||||
memory: document.querySelector("#ram").value
|
memory: document.querySelector("#ram").value
|
||||||
|
@@ -1,31 +1,40 @@
|
|||||||
export function dialog (header, body, onclose = async (result, form) => { }) {
|
/**
|
||||||
const dialog = document.createElement("dialog");
|
* Spawn modal dialog from template node. Assumes the following structure:
|
||||||
dialog.innerHTML = `
|
* <template>
|
||||||
<p class="w3-large" id="prompt" style="text-align: center;"></p>
|
* <dialog>
|
||||||
<div id="body"></div>
|
* <p id="prompt"></p>
|
||||||
<div class="w3-center w3-container">
|
* <div id="body">
|
||||||
<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>
|
* <form id="form"> ... </form>
|
||||||
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
|
* </div>
|
||||||
</div>
|
* <div id="controls">
|
||||||
`;
|
* <button value="..." form="form"
|
||||||
dialog.className = "w3-container w3-card w3-border-0";
|
* <button value="..." form="form"
|
||||||
dialog.querySelector("#prompt").innerText = header;
|
* ...
|
||||||
dialog.querySelector("#body").innerHTML = body;
|
* </div>
|
||||||
|
* </dialog>
|
||||||
|
* </template>
|
||||||
|
* Where prompt is the modal dialog's prompt or header,
|
||||||
|
* 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) => { }) {
|
||||||
|
const dialog = template.content.querySelector("dialog").cloneNode(true);
|
||||||
|
document.body.append(dialog);
|
||||||
dialog.addEventListener("close", async () => {
|
dialog.addEventListener("close", async () => {
|
||||||
const formElem = dialog.querySelector("form");
|
const formElem = dialog.querySelector("form");
|
||||||
const formData = formElem ? new FormData(formElem) : null;
|
const formData = formElem ? new FormData(formElem) : null;
|
||||||
await onclose(dialog.returnValue, formData);
|
await onclose(dialog.returnValue, formData);
|
||||||
|
formElem.reset();
|
||||||
|
dialog.close();
|
||||||
dialog.parentElement.removeChild(dialog);
|
dialog.parentElement.removeChild(dialog);
|
||||||
});
|
});
|
||||||
if (!dialog.querySelector("form")) {
|
if (!dialog.querySelector("form")) {
|
||||||
dialog.querySelector("#confirm").addEventListener("click", async (e) => {
|
for (const control of dialog.querySelector("#controls").childNodes) {
|
||||||
e.preventDefault();
|
control.addEventListener("click", async (e) => {
|
||||||
dialog.close(e.target.value);
|
e.preventDefault();
|
||||||
});
|
dialog.close(e.target.value);
|
||||||
dialog.querySelector("#cancel").addEventListener("click", async (e) => {
|
});
|
||||||
e.preventDefault();
|
}
|
||||||
dialog.close(e.target.value);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
document.body.append(dialog);
|
document.body.append(dialog);
|
||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
@@ -33,23 +42,114 @@ export function dialog (header, body, onclose = async (result, form) => { }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function alert (message) {
|
export function alert (message) {
|
||||||
const dialog = document.createElement("dialog");
|
const dialog = document.querySelector("#alert-dialog");
|
||||||
dialog.innerHTML = `
|
if (dialog == null) {
|
||||||
<form method="dialog">
|
const dialog = document.createElement("dialog");
|
||||||
<p class="w3-center" style="margin-bottom: 0px;">${message}</p>
|
dialog.id = "alert-dialog";
|
||||||
<div class="w3-center">
|
dialog.innerHTML = `
|
||||||
<button class="w3-button w3-margin" id="submit">OK</button>
|
<form method="dialog">
|
||||||
</div>
|
<p class="w3-center" style="margin-bottom: 0px;">${message}</p>
|
||||||
</form>
|
<div class="w3-center">
|
||||||
`;
|
<button class="w3-button w3-margin" id="submit">OK</button>
|
||||||
dialog.className = "w3-container w3-card w3-border-0";
|
</div>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
dialog.className = "w3-container w3-card w3-border-0";
|
||||||
|
document.body.append(dialog);
|
||||||
|
dialog.showModal();
|
||||||
|
dialog.addEventListener("close", () => {
|
||||||
|
dialog.parentElement.removeChild(dialog);
|
||||||
|
});
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.error("Attempted to create a new alert while one already exists!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.body.append(dialog);
|
class ErrorDialog extends HTMLElement {
|
||||||
dialog.showModal();
|
shadowRoot = null;
|
||||||
|
dialog = null;
|
||||||
|
errors = null;
|
||||||
|
|
||||||
dialog.addEventListener("close", () => {
|
constructor () {
|
||||||
dialog.parentElement.removeChild(dialog);
|
super();
|
||||||
});
|
this.shadowRoot = this.attachShadow({ mode: "open" });
|
||||||
|
this.shadowRoot.innerHTML = `
|
||||||
|
<link rel="stylesheet" href="modules/w3.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<link rel="stylesheet" href="css/form.css">
|
||||||
|
<style>
|
||||||
|
#errors {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
max-height: 20lh;
|
||||||
|
min-height: 20lh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
#errors * {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
<button class="w3-button w3-margin" type="submit" value="copy">Copy</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
this.dialog = this.shadowRoot.querySelector("dialog");
|
||||||
|
this.errors = this.shadowRoot.querySelector("#errors")
|
||||||
|
|
||||||
|
for (const control of this.shadowRoot.querySelector("#controls").childNodes) {
|
||||||
|
control.addEventListener("click", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.dialog.close(e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialog.addEventListener("close", () => {
|
||||||
|
if (this.dialog.returnValue == "ok") {}
|
||||||
|
else if (this.dialog.returnValue == "copy") {
|
||||||
|
let errors = ""
|
||||||
|
for (const error of this.errors.childNodes) {
|
||||||
|
errors += `${error.innerText}\n`
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(errors)
|
||||||
|
}
|
||||||
|
this.parentElement.removeChild(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
appendError (error) {
|
||||||
|
error = `${(new Date()).toUTCString()}: ${error}`;
|
||||||
|
const p = document.createElement("p");
|
||||||
|
p.innerText = error;
|
||||||
|
this.errors.appendChild(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal () {
|
||||||
|
this.dialog.showModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("error-dialog", ErrorDialog);
|
||||||
|
|
||||||
|
export function error (message) {
|
||||||
|
let dialog = document.querySelector("error-dialog");
|
||||||
|
if (dialog == null) {
|
||||||
|
dialog = document.createElement("error-dialog");
|
||||||
|
document.body.append(dialog);
|
||||||
|
dialog.appendError(message);
|
||||||
|
dialog.showModal();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dialog.appendError(message);
|
||||||
|
dialog.showModal();
|
||||||
|
}
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
@@ -48,7 +48,7 @@ class DraggableContainer extends HTMLElement {
|
|||||||
|
|
||||||
get value () {
|
get value () {
|
||||||
const value = [];
|
const value = [];
|
||||||
this.content.childNodes.forEach((element) => {
|
this.content.querySelectorAll(".draggable-item").forEach((element) => {
|
||||||
if (element.dataset.value) {
|
if (element.dataset.value) {
|
||||||
value.push(element.dataset.value);
|
value.push(element.dataset.value);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { requestPVE, requestAPI, goToPage, setAppearance, getSearchSettings, goToURL, requestDash } from "./utils.js";
|
import { requestPVE, requestAPI, setAppearance, getSearchSettings, requestDash, setSVGSrc, setSVGAlt } from "./utils.js";
|
||||||
import { alert, dialog } from "./dialog.js";
|
import { alert, dialog } from "./dialog.js";
|
||||||
import { setupClientSync } from "./clientsync.js";
|
import { setupClientSync } from "./clientsync.js";
|
||||||
import wfaInit from "../modules/wfa.js";
|
import wfaInit from "../modules/wfa.js";
|
||||||
@@ -11,7 +11,7 @@ async function init () {
|
|||||||
wfaInit("modules/wfa.wasm");
|
wfaInit("modules/wfa.wasm");
|
||||||
initInstances();
|
initInstances();
|
||||||
|
|
||||||
document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd);
|
document.querySelector("#instance-add").addEventListener("click", handleInstanceAddButton);
|
||||||
document.querySelector("#vm-search").addEventListener("input", sortInstances);
|
document.querySelector("#vm-search").addEventListener("input", sortInstances);
|
||||||
|
|
||||||
setupClientSync(refreshInstances);
|
setupClientSync(refreshInstances);
|
||||||
@@ -122,35 +122,46 @@ class InstanceCard extends HTMLElement {
|
|||||||
const powerButton = this.shadowRoot.querySelector("#power-btn");
|
const powerButton = this.shadowRoot.querySelector("#power-btn");
|
||||||
if (powerButton.classList.contains("clickable")) {
|
if (powerButton.classList.contains("clickable")) {
|
||||||
powerButton.onclick = this.handlePowerButton.bind(this);
|
powerButton.onclick = this.handlePowerButton.bind(this);
|
||||||
}
|
powerButton.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
const configButton = this.shadowRoot.querySelector("#configure-btn");
|
event.preventDefault();
|
||||||
if (configButton.classList.contains("clickable")) {
|
this.handlePowerButton();
|
||||||
configButton.onclick = this.handleConfigButton.bind(this);
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const consoleButton = this.shadowRoot.querySelector("#console-btn");
|
|
||||||
if (consoleButton.classList.contains("clickable")) {
|
|
||||||
consoleButton.classList.add("clickable");
|
|
||||||
consoleButton.onclick = this.handleConsoleButton.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
|
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
|
||||||
if (deleteButton.classList.contains("clickable")) {
|
if (deleteButton.classList.contains("clickable")) {
|
||||||
deleteButton.onclick = this.handleDeleteButton.bind(this);
|
deleteButton.onclick = this.handleDeleteButton.bind(this);
|
||||||
|
deleteButton.onkeydown = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
this.handleDeleteButton();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStatusLoading () {
|
||||||
|
this.status = "loading";
|
||||||
|
const statusicon = this.shadowRoot.querySelector("#status");
|
||||||
|
const powerbtn = this.shadowRoot.querySelector("#power-btn");
|
||||||
|
setSVGSrc(statusicon, "images/status/loading.svg");
|
||||||
|
setSVGAlt(statusicon, "instance is loading");
|
||||||
|
setSVGSrc(powerbtn, "images/status/loading.svg");
|
||||||
|
setSVGAlt(powerbtn, "");
|
||||||
|
}
|
||||||
|
|
||||||
async handlePowerButton () {
|
async handlePowerButton () {
|
||||||
if (!this.actionLock) {
|
if (!this.actionLock) {
|
||||||
const header = `${this.status === "running" ? "Stop" : "Start"} VM ${this.vmid}`;
|
const template = this.shadowRoot.querySelector("#power-dialog");
|
||||||
const body = `<p>Are you sure you want to ${this.status === "running" ? "stop" : "start"} VM ${this.vmid}</p>`;
|
dialog(template, async (result, form) => {
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.actionLock = true;
|
this.actionLock = true;
|
||||||
const targetAction = this.status === "running" ? "stop" : "start";
|
const targetAction = this.status === "running" ? "stop" : "start";
|
||||||
|
|
||||||
const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid });
|
const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid });
|
||||||
|
this.setStatusLoading();
|
||||||
|
|
||||||
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
|
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
|
||||||
@@ -175,26 +186,10 @@ class InstanceCard extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfigButton () {
|
|
||||||
if (!this.actionLock && this.status === "stopped") { // if the action lock is false, and the node is stopped, then navigate to the config page with the node info in the search query
|
|
||||||
goToPage("config", { node: this.node.name, type: this.type, vmid: this.vmid });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConsoleButton () {
|
|
||||||
if (!this.actionLock && this.status === "running") {
|
|
||||||
const data = { console: `${this.type === "qemu" ? "kvm" : "lxc"}`, vmid: this.vmid, vmname: this.name, node: this.node.name, resize: "off", cmd: "" };
|
|
||||||
data[`${this.type === "qemu" ? "novnc" : "xtermjs"}`] = 1;
|
|
||||||
goToURL(window.PVE, data, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDeleteButton () {
|
handleDeleteButton () {
|
||||||
if (!this.actionLock && this.status === "stopped") {
|
if (!this.actionLock && this.status === "stopped") {
|
||||||
const header = `Delete VM ${this.vmid}`;
|
const template = this.shadowRoot.querySelector("#delete-dialog");
|
||||||
const body = `<p>Are you sure you want to <strong>delete</strong> VM ${this.vmid}</p>`;
|
dialog(template, async (result, form) => {
|
||||||
|
|
||||||
dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.actionLock = true;
|
this.actionLock = true;
|
||||||
|
|
||||||
@@ -224,7 +219,7 @@ async function getInstancesFragment () {
|
|||||||
async function refreshInstances () {
|
async function refreshInstances () {
|
||||||
let instances = await getInstancesFragment();
|
let instances = await getInstancesFragment();
|
||||||
if (instances.status !== 200) {
|
if (instances.status !== 200) {
|
||||||
alert("Error fetching instances.");
|
error(`Error fetching instances: ${instances.status} ${instances.error !== undefined ? instances.error : ""}`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
instances = instances.data;
|
instances = instances.data;
|
||||||
@@ -258,9 +253,9 @@ function sortInstances () {
|
|||||||
if (substrInc) {
|
if (substrInc) {
|
||||||
const substrStartIndex = item.indexOf(query);
|
const substrStartIndex = item.indexOf(query);
|
||||||
const queryLength = query.length;
|
const queryLength = query.length;
|
||||||
const remaining = item.length - substrInc - queryLength;
|
const remaining = item.length - substrInc - queryLength + 1;
|
||||||
const alignment = `${"X".repeat(substrStartIndex)}${"M".repeat(queryLength)}${"X".repeat(remaining)}`;
|
const alignment = `${"X".repeat(substrStartIndex)}${"M".repeat(queryLength)}${"X".repeat(remaining)}`;
|
||||||
return { score: 1, alignment };
|
return { score: -1, alignment };
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const alignment = `${"X".repeat(item.length)}`;
|
const alignment = `${"X".repeat(item.length)}`;
|
||||||
@@ -277,8 +272,8 @@ function sortInstances () {
|
|||||||
};
|
};
|
||||||
criteria = (item, query) => {
|
criteria = (item, query) => {
|
||||||
// lower is better
|
// lower is better
|
||||||
const { score, CIGAR } = global.wfAlign(query, item, penalties, true);
|
const { score, CIGAR } = global.wfa.wfAlign(query, item, penalties, true);
|
||||||
const alignment = global.DecodeCIGAR(CIGAR);
|
const alignment = global.wfa.DecodeCIGAR(CIGAR);
|
||||||
return { score: score / item.length, alignment };
|
return { score: score / item.length, alignment };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -313,47 +308,9 @@ function sortInstances () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInstanceAdd () {
|
async function handleInstanceAddButton () {
|
||||||
const header = "Create New Instance";
|
const template = document.querySelector("#create-instance-dialog");
|
||||||
|
const d = dialog(template, async (result, form) => {
|
||||||
const body = `
|
|
||||||
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
|
||||||
<label for="type">Instance Type</label>
|
|
||||||
<select class="w3-select w3-border" name="type" id="type" required>
|
|
||||||
<option value="lxc">Container</option>
|
|
||||||
<option value="qemu">Virtual Machine</option>
|
|
||||||
</select>
|
|
||||||
<label for="node">Node</label>
|
|
||||||
<select class="w3-select w3-border" name="node" id="node" required></select>
|
|
||||||
<label for="name">Name</label>
|
|
||||||
<input class="w3-input w3-border" name="name" id="name" required>
|
|
||||||
<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>
|
|
||||||
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16", step="1" required>
|
|
||||||
<p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p>
|
|
||||||
<label class="container-specific none" for="swap">Swap (MiB)</label>
|
|
||||||
<input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled>
|
|
||||||
<label class="container-specific none" for="template-image">Template Image</label>
|
|
||||||
<select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select>
|
|
||||||
<label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label>
|
|
||||||
<select class="w3-select w3-border container-specific none" name="rootfs-storage" id="rootfs-storage" required disabled></select>
|
|
||||||
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
|
|
||||||
<input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled>
|
|
||||||
<label class="container-specific none" for="password">Password</label>
|
|
||||||
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled>
|
|
||||||
<label class="container-specific none" for="confirm-password">Confirm Password</label>
|
|
||||||
<input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" required disabled>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const templates = await requestAPI("/user/ct-templates", "GET");
|
|
||||||
|
|
||||||
const d = dialog(header, body, async (result, form) => {
|
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const body = {
|
const body = {
|
||||||
name: form.get("name"),
|
name: form.get("name"),
|
||||||
@@ -382,6 +339,8 @@ async function handleInstanceAdd () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templates = await requestAPI("/user/ct-templates", "GET");
|
||||||
|
|
||||||
const typeSelect = d.querySelector("#type");
|
const typeSelect = d.querySelector("#type");
|
||||||
typeSelect.selectedIndex = -1;
|
typeSelect.selectedIndex = -1;
|
||||||
typeSelect.addEventListener("change", () => {
|
typeSelect.addEventListener("change", () => {
|
||||||
@@ -398,6 +357,10 @@ async function handleInstanceAdd () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
d.querySelectorAll(".container-specific").forEach((element) => {
|
||||||
|
element.classList.add("none");
|
||||||
|
element.disabled = true;
|
||||||
|
});
|
||||||
|
|
||||||
const rootfsContent = "rootdir";
|
const rootfsContent = "rootdir";
|
||||||
const rootfsStorage = d.querySelector("#rootfs-storage");
|
const rootfsStorage = d.querySelector("#rootfs-storage");
|
||||||
@@ -407,6 +370,7 @@ async function handleInstanceAdd () {
|
|||||||
const userCluster = await requestAPI("/user/config/cluster", "GET");
|
const userCluster = await requestAPI("/user/config/cluster", "GET");
|
||||||
|
|
||||||
const nodeSelect = d.querySelector("#node");
|
const nodeSelect = d.querySelector("#node");
|
||||||
|
nodeSelect.innerHTML = "";
|
||||||
const clusterNodes = await requestPVE("/nodes", "GET");
|
const clusterNodes = await requestPVE("/nodes", "GET");
|
||||||
const allowedNodes = Object.keys(userCluster.nodes);
|
const allowedNodes = Object.keys(userCluster.nodes);
|
||||||
clusterNodes.data.forEach((element) => {
|
clusterNodes.data.forEach((element) => {
|
||||||
@@ -418,6 +382,7 @@ async function handleInstanceAdd () {
|
|||||||
nodeSelect.addEventListener("change", async () => { // change rootfs storage based on node
|
nodeSelect.addEventListener("change", async () => { // change rootfs storage based on node
|
||||||
const node = nodeSelect.value;
|
const node = nodeSelect.value;
|
||||||
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
||||||
|
rootfsStorage.innerHTML = "";
|
||||||
storage.data.forEach((element) => {
|
storage.data.forEach((element) => {
|
||||||
if (element.content.includes(rootfsContent)) {
|
if (element.content.includes(rootfsContent)) {
|
||||||
rootfsStorage.add(new Option(element.storage));
|
rootfsStorage.add(new Option(element.storage));
|
||||||
@@ -447,6 +412,7 @@ async function handleInstanceAdd () {
|
|||||||
|
|
||||||
// add user pools to selector
|
// add user pools to selector
|
||||||
const poolSelect = d.querySelector("#pool");
|
const poolSelect = d.querySelector("#pool");
|
||||||
|
poolSelect.innerHTML = "";
|
||||||
const userPools = Object.keys(userCluster.pools);
|
const userPools = Object.keys(userCluster.pools);
|
||||||
userPools.forEach((element) => {
|
userPools.forEach((element) => {
|
||||||
poolSelect.add(new Option(element));
|
poolSelect.add(new Option(element));
|
||||||
@@ -460,13 +426,14 @@ async function handleInstanceAdd () {
|
|||||||
}
|
}
|
||||||
templateImage.selectedIndex = -1;
|
templateImage.selectedIndex = -1;
|
||||||
|
|
||||||
|
// setup custom password checker for containers
|
||||||
const password = d.querySelector("#password");
|
const password = d.querySelector("#password");
|
||||||
const confirmPassword = d.querySelector("#confirm-password");
|
const confirmPassword = d.querySelector("#confirm-password");
|
||||||
|
|
||||||
function validatePassword () {
|
function validatePassword () {
|
||||||
confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : "");
|
confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
password.addEventListener("change", validatePassword);
|
password.addEventListener("change", validatePassword);
|
||||||
confirmPassword.addEventListener("keyup", validatePassword);
|
confirmPassword.addEventListener("keyup", validatePassword);
|
||||||
|
|
||||||
|
d.showModal();
|
||||||
}
|
}
|
||||||
|
@@ -81,7 +81,11 @@ async function request (url, content) {
|
|||||||
const response = await fetch(url, content);
|
const response = await fetch(url, content);
|
||||||
const contentType = response.headers.get("Content-Type");
|
const contentType = response.headers.get("Content-Type");
|
||||||
let data = null;
|
let data = null;
|
||||||
if (contentType.includes("application/json")) {
|
|
||||||
|
if (contentType === null) {
|
||||||
|
data = {};
|
||||||
|
}
|
||||||
|
else if (contentType.includes("application/json")) {
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
data.status = response.status;
|
data.status = response.status;
|
||||||
}
|
}
|
||||||
@@ -94,8 +98,9 @@ async function request (url, content) {
|
|||||||
data.status = response.status;
|
data.status = response.status;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
data = response;
|
data = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return { status: response.status, error: data ? data.error : response.status };
|
return { status: response.status, error: data ? data.error : response.status };
|
||||||
}
|
}
|
||||||
@@ -114,20 +119,6 @@ export function goToPage (page, data = null) {
|
|||||||
window.location.href = `${page}${data ? "?" : ""}${params}`;
|
window.location.href = `${page}${data ? "?" : ""}${params}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function goToURL (href, data = {}, newwindow = false) {
|
|
||||||
const url = new URL(href);
|
|
||||||
for (const k in data) {
|
|
||||||
url.searchParams.append(k, data[k]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newwindow) {
|
|
||||||
window.open(url, document.title, "height=480,width=848");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
window.location.assign(url.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getURIData () {
|
export function getURIData () {
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
return Object.fromEntries(url.searchParams);
|
return Object.fromEntries(url.searchParams);
|
||||||
|
125
web/templates/backups.go.tmpl
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{{define "backup-card"}}
|
||||||
|
<backup-card data-volid="{{.Volid}}">
|
||||||
|
<template shadowrootmode="open">
|
||||||
|
<link rel="stylesheet" href="modules/w3.css">
|
||||||
|
<link rel="stylesheet" href="css/style.css">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="w3-row" style="margin-top: 1em; margin-bottom: 1em;">
|
||||||
|
<p class="w3-col l2 m4 s8">{{.TimeFormatted}}</p>
|
||||||
|
<p class="w3-col l6 m6 hide-small">{{.Notes}}</p>
|
||||||
|
<p class="w3-col l2 hide-medium">{{.SizeFormatted}}</p>
|
||||||
|
<div class="w3-col l2 m2 s4 flex row nowrap" style="height: 1lh;">
|
||||||
|
<svg id="edit-btn" class="clickable" aria-label="change notes"><use href="images/actions/backups/config.svg#symb"></svg>
|
||||||
|
<svg id="delete-btn" class="clickable" aria-label="delete backup" role="button" tabindex=0><use href="images/actions/backups/delete-active.svg#symb"></svg>
|
||||||
|
<svg id="restore-btn" class="clickable" aria-label="restore from backup" role="button" tabindex=0><use href="images/actions/backups/restore.svg#symb"></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template id="edit-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;">
|
||||||
|
Edit Backup
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto;" id="form">
|
||||||
|
<label for="rate">Notes</label>
|
||||||
|
<textarea id="notes" name="notes" class="w3-input w3-border">{{.Notes}}</textarea>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
<template id="delete-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;">
|
||||||
|
Delete Backup
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<p>
|
||||||
|
Are you sure you want to <strong>delete</strong> the backup made at {{.TimeFormatted}}?
|
||||||
|
</p>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
<template id="restore-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;">
|
||||||
|
Restore From Backup?
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<p>
|
||||||
|
Are you sure you want to <strong>restore</strong> from the backup made at {{.TimeFormatted}}?
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<strong>WARNING: Restoring from a backup will WIPE disks NOT contained in the backup!!!</strong>
|
||||||
|
</p>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</backup-card>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "backups-add-backup"}}
|
||||||
|
<button type="button" id="backup-add" class="w3-button" aria-label="Create Backup">
|
||||||
|
<span class="large" style="margin: 0;">Create Backup</span>
|
||||||
|
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Create Backup"><use href="images/actions/network/add.svg#symb"></use></svg>
|
||||||
|
</button>
|
||||||
|
<template id="create-backup-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;">
|
||||||
|
Create Backup
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto;" id="form">
|
||||||
|
<label for="rate">Notes</label>
|
||||||
|
<textarea id="notes" name="notes" class="w3-input w3-border">{{.Notes}}</textarea>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
{{end}}
|
@@ -4,7 +4,6 @@
|
|||||||
<title>{{.global.Organization}} - dashboard</title>
|
<title>{{.global.Organization}} - dashboard</title>
|
||||||
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
|
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
|
||||||
<link rel="stylesheet" href="modules/w3.css">
|
<link rel="stylesheet" href="modules/w3.css">
|
||||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
|
||||||
<script>
|
<script>
|
||||||
window.PVE = "{{.global.PVE}}";
|
window.PVE = "{{.global.PVE}}";
|
||||||
window.API = "{{.global.API}}";
|
window.API = "{{.global.API}}";
|
||||||
|
@@ -27,22 +27,75 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "volumes"}}
|
{{define "volumes"}}
|
||||||
{{range $k,$v := .}}
|
{{range $k,$v := .Volumes}}
|
||||||
{{if eq $v.Type "rootfs"}}
|
{{if eq $v.Type "rootfs"}}
|
||||||
{{ template "volume-rootfs" Map "Name" $k "Volume" $v}}
|
{{ template "volume-rootfs" Map "Name" $k "Volume" $v "InstanceType" $.InstanceType}}
|
||||||
{{else if eq $v.Type "mp"}}
|
{{else if eq $v.Type "mp"}}
|
||||||
{{ template "volume-mp" Map "Name" $k "Volume" $v}}
|
{{ template "volume-mp" Map "Name" $k "Volume" $v "InstanceType" $.InstanceType}}
|
||||||
{{else if eq $v.Type "ide"}}
|
{{else if eq $v.Type "ide"}}
|
||||||
{{ template "volume-ide" Map "Name" $k "Volume" $v}}
|
{{ template "volume-ide" Map "Name" $k "Volume" $v "InstanceType" $.InstanceType}}
|
||||||
{{else if or (eq $v.Type "scsi") (eq $v.Type "sata")}}
|
{{else if or (eq $v.Type "scsi") (eq $v.Type "sata")}}
|
||||||
{{ template "volume-scsi" Map "Name" $k "Volume" $v}}
|
{{ template "volume-scsi" Map "Name" $k "Volume" $v "InstanceType" $.InstanceType}}
|
||||||
{{else if eq $v.Type "unused"}}
|
{{else if eq $v.Type "unused"}}
|
||||||
{{ template "volume-unused" Map "Name" $k "Volume" $v}}
|
{{ template "volume-unused" Map "Name" $k "Volume" $v "InstanceType" $.InstanceType}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "volumes-add-disk"}}
|
||||||
|
<button type="button" id="disk-add" class="w3-button" aria-label="Add New Disk">
|
||||||
|
<span class="large" style="margin: 0;">Add Disk</span>
|
||||||
|
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Disk"><use href="images/actions/disk/add-disk.svg#symb"></use></svg>
|
||||||
|
</button>
|
||||||
|
<template id="add-disk-dialog">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Create New Disk
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
{{if eq .config.Type "VM"}}
|
||||||
|
<label for="device">SCSI</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="30" value="0" required>
|
||||||
|
{{else}}
|
||||||
|
<label for="device">MP</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="255" value="0" required>
|
||||||
|
{{end}}
|
||||||
|
<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required></select>
|
||||||
|
<label for="size">Size (GiB)</label><input class="w3-input w3-border" name="size" id="size" type="number" min="0" max="131072" required>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "volumes-add-cd"}}
|
||||||
|
<button type="button" id="cd-add" class="w3-button" aria-label="Add New CD">
|
||||||
|
<span class="large" style="margin: 0;">Mount CD</span>
|
||||||
|
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New CDROM"><use href="images/actions/disk/add-cd.svg#symb"></use></svg>
|
||||||
|
</button>
|
||||||
|
<template id="add-cd-dialog">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Mount a CDROM
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="device">IDE</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="3" required>
|
||||||
|
<label for="iso-select">Image</label><select class="w3-select w3-border" name="iso-select" id="iso-select" required></select>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "volume-rootfs"}}
|
{{define "volume-rootfs"}}
|
||||||
<svg data-volume={{.Name}} xmlns="http://www.w3.org/2000/svg" aria-label="Drive"><use href="images/resources/drive.svg#symb"></svg>
|
<svg data-volume={{.Name}} xmlns="http://www.w3.org/2000/svg" aria-label="Drive"><use href="images/resources/drive.svg#symb"></svg>
|
||||||
<p>{{.Name}}</p>
|
<p>{{.Name}}</p>
|
||||||
@@ -108,6 +161,23 @@
|
|||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Move {{.Name}}"><use href="images/actions/disk/move-active.svg#symb"></svg>
|
<svg class="clickable" aria-label="Move {{.Name}}"><use href="images/actions/disk/move-active.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Move {{.Name}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select"></select>
|
||||||
|
<label for="delete-check">Delete Source</label><input class="w3-input w3-border" name="delete-check" id="delete-check" type="checkbox" checked required>
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</volume-action>
|
</volume-action>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -126,6 +196,23 @@
|
|||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Resize {{.Name}}"><use href="images/actions/disk/resize-active.svg#symb"></svg>
|
<svg class="clickable" aria-label="Resize {{.Name}}"><use href="images/actions/disk/resize-active.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Resize {{.Name}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="size-increment">Size Increment (GiB)</label>
|
||||||
|
<input class="w3-input w3-border" name="size-increment" id="size-increment" type="number" min="0" max="131072">
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</volume-action>
|
</volume-action>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -144,6 +231,22 @@
|
|||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Delete {{.Name}}"><use href="images/actions/disk/delete-active.svg#symb"></svg>
|
<svg class="clickable" aria-label="Delete {{.Name}}"><use href="images/actions/disk/delete-active.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Delete {{.Name}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<p>Are you sure you want to <strong>delete</strong> disk {{.Name}}?</p>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</volume-action>
|
</volume-action>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -162,6 +265,30 @@
|
|||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Attach {{.Name}}"><use href="images/actions/disk/attach.svg#symb"></svg>
|
<svg class="clickable" aria-label="Attach {{.Name}}"><use href="images/actions/disk/attach.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Attach {{.Name}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
{{if eq .InstanceType "VM"}}
|
||||||
|
<label for="device">SCSI</label>
|
||||||
|
<input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="30" required>
|
||||||
|
{{else}}
|
||||||
|
<label for="device">MP</label>
|
||||||
|
<input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="255" required>
|
||||||
|
<label for="device">Path</label>
|
||||||
|
<input class="w3-input w3-border" name="mp" id="mp" required>
|
||||||
|
{{end}}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</volume-action>
|
</volume-action>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -171,6 +298,22 @@
|
|||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Detach {{.Name}}"><use href="images/actions/disk/detach.svg#symb"></svg>
|
<svg class="clickable" aria-label="Detach {{.Name}}"><use href="images/actions/disk/detach.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Detach {{.Name}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<p>Are you sure you want to detach disk {{.Name}}?</p>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</volume-action>
|
</volume-action>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -190,6 +333,33 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "nets-add-net"}}
|
||||||
|
<button type="button" id="network-add" class="w3-button" aria-label="Add New Network Interface">
|
||||||
|
<span class="large" style="margin: 0;">Add Network</span>
|
||||||
|
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Network Interface"><use href="images/actions/network/add.svg#symb"></use></svg>
|
||||||
|
</button>
|
||||||
|
<template id="add-net-dialog">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Create Network Interface
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="netid">Interface ID</label><input type="number" id="netid" name="netid" class="w3-input w3-border">
|
||||||
|
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
|
||||||
|
{{if eq .config.Type "CT"}}
|
||||||
|
<label for="name">Interface Name</label><input type="text" id="name" name="name" class="w3-input w3-border">
|
||||||
|
{{end}}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "net"}}
|
{{define "net"}}
|
||||||
<svg data-network="{{.Net_ID}}" aria-label="Net {{.Net_ID}}"><use href="images/resources/network.svg#symb"></svg>
|
<svg data-network="{{.Net_ID}}" aria-label="Net {{.Net_ID}}"><use href="images/resources/network.svg#symb"></svg>
|
||||||
<p>{{.Net_ID}}</p>
|
<p>{{.Net_ID}}</p>
|
||||||
@@ -199,12 +369,44 @@
|
|||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Configure Net {{.Net_ID}}"><use href="images/actions/network/config.svg#symb"></svg>
|
<svg class="clickable" aria-label="Configure Net {{.Net_ID}}"><use href="images/actions/network/config.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Edit {{.Net_ID}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</network-action>
|
</network-action>
|
||||||
<network-action data-type="delete" data-network="{{.Net_ID}}" data-value="{{.Value}}">
|
<network-action data-type="delete" data-network="{{.Net_ID}}" data-value="{{.Value}}">
|
||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Delete Net {{.Net_ID}}"><use href="images/actions/network/delete-active.svg#symb"></svg>
|
<svg class="clickable" aria-label="Delete Net {{.Net_ID}}"><use href="images/actions/network/delete-active.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Delete {{.Net_ID}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<p>Are you sure you want to <strong>delete</strong> {{.Net_ID}}?</p>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</network-action>
|
</network-action>
|
||||||
</div>
|
</div>
|
||||||
@@ -216,6 +418,31 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{define "devices-add-device"}}
|
||||||
|
<button type="button" id="device-add" class="w3-button" aria-label="Add New PCIe Device">
|
||||||
|
<span class="large" style="margin: 0;">Add Device</span>
|
||||||
|
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New PCIe Device"><use href="images/actions/device/add.svg#symb"></use></svg>
|
||||||
|
</button>
|
||||||
|
<template id="add-device-dialog">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Add Expansion Card
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="hostpci">Device Bus</label><input type="number" id="hostpci" name="hostpci" class="w3-input w3-border">
|
||||||
|
<label for="device">Device</label><select id="device" name="device" required></select>
|
||||||
|
<label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "device"}}
|
{{define "device"}}
|
||||||
<svg data-device="{{.Device_ID}}" aria-label="Device {{.Device_ID}}"><use href="images/resources/device.svg#symb"></svg>
|
<svg data-device="{{.Device_ID}}" aria-label="Device {{.Device_ID}}"><use href="images/resources/device.svg#symb"></svg>
|
||||||
<p>{{.Device_ID}}</p>
|
<p>{{.Device_ID}}</p>
|
||||||
@@ -225,63 +452,86 @@
|
|||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Configure Device {{.Device_ID}}"><use href="images/actions/device/config.svg#symb"></svg>
|
<svg class="clickable" aria-label="Configure Device {{.Device_ID}}"><use href="images/actions/device/config.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
Edit Expansion Card {{.Device_ID}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<label for="device">Device</label><select id="device" name="device" required></select><label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
|
||||||
|
</form>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</device-action>
|
</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="{{.Value}}">
|
||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<svg class="clickable" aria-label="Delete Device {{.Device_ID}}"><use href="images/actions/device/delete-active.svg#symb"></svg>
|
<svg class="clickable" aria-label="Delete Device {{.Device_ID}}"><use href="images/actions/device/delete-active.svg#symb"></svg>
|
||||||
|
<template id="dialog-template">
|
||||||
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
|
remove Expansion Card {{.Device_ID}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<p>Are you sure you want to <strong>remove</strong> {{.Device_ID}}?</p>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</device-action>
|
</device-action>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "boot"}}
|
{{define "boot"}}
|
||||||
<draggable-container id="enabled" data-group="boot">
|
{{template "boot-container" Map "ID" "enabled" "Name" "Enabled" "Targets" .Enabled}}
|
||||||
<template shadowrootmode="open">
|
|
||||||
{{template "boot-style"}}
|
|
||||||
<label>Enabled</label>
|
|
||||||
<div id="wrapper" style="padding-bottom: 1em;">
|
|
||||||
{{range .Enabled}}
|
|
||||||
{{template "boot-target" .}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</draggable-container>
|
|
||||||
<hr style="padding: 0; margin: 0;">
|
<hr style="padding: 0; margin: 0;">
|
||||||
<draggable-container id="disabled" data-group="boot">
|
{{template "boot-container" Map "ID" "disabled" "Name" "Disabled" "Targets" .Disabled}}
|
||||||
<template shadowrootmode="open">
|
|
||||||
{{template "boot-style"}}
|
|
||||||
<label>Disabled</label>
|
|
||||||
<div id="wrapper" style="padding-bottom: 1em;">
|
|
||||||
{{range .Disabled}}
|
|
||||||
{{template "boot-target" .}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</draggable-container>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "boot-style"}}
|
{{define "boot-container"}}
|
||||||
<style>
|
<draggable-container id="{{.ID}}" data-group="boot">
|
||||||
div.draggable-item.ghost {
|
<template shadowrootmode="open">
|
||||||
border: 1px dashed var(--main-text-color);
|
<style>
|
||||||
border-radius: 5px;
|
* {
|
||||||
margin: -1px;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
div.draggable-item {
|
div.draggable-item.ghost {
|
||||||
cursor: grab;
|
border: 1px dashed var(--main-text-color);
|
||||||
}
|
border-radius: 5px;
|
||||||
div.draggable-item svg {
|
margin: -1px;
|
||||||
height: 1em;
|
}
|
||||||
width: 1em;
|
div.draggable-item {
|
||||||
}
|
cursor: grab;
|
||||||
* {
|
}
|
||||||
-webkit-box-sizing: border-box;
|
div.draggable-item svg {
|
||||||
-moz-box-sizing: border-box;
|
height: 1em;
|
||||||
box-sizing: border-box;
|
width: 1em;
|
||||||
}
|
}
|
||||||
</style>
|
#wrapper {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<label>{{.Name}}</label>
|
||||||
|
<div id="wrapper">
|
||||||
|
{{range .Targets}}
|
||||||
|
{{template "boot-target" .}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable-container>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{define "boot-target"}}
|
{{define "boot-target"}}
|
||||||
|
@@ -2,61 +2,159 @@
|
|||||||
<instance-card data-type="{{.Type}}" data-status="{{.Status}}" data-vmid="{{.VMID}}" data-name="{{.Name}}" data-node="{{.Node}}" data-nodestatus="{{.NodeStatus}}">
|
<instance-card data-type="{{.Type}}" data-status="{{.Status}}" data-vmid="{{.VMID}}" data-name="{{.Name}}" data-node="{{.Node}}" data-nodestatus="{{.NodeStatus}}">
|
||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="modules/w3.css">
|
<link rel="stylesheet" href="modules/w3.css">
|
||||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
a, svg {
|
||||||
|
line-height: 1em;
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
#instance-name {
|
||||||
|
overflow-x: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
width: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.flex { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.row { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.nowrap { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
@media screen and (width >=993px) {
|
||||||
|
.hide-large {display: none !important;}
|
||||||
|
}
|
||||||
|
@media screen and (width <=993px) and (width >=601px){
|
||||||
|
.hide-large {display: none !important;}
|
||||||
|
.hide-medium {display:none !important}
|
||||||
|
}
|
||||||
|
@media screen and (width <=601px) {
|
||||||
|
.hide-large {display: none !important;}
|
||||||
|
.hide-medium {display:none !important}
|
||||||
|
.hide-small {display:none !important}
|
||||||
|
}
|
||||||
|
@media screen and (width <= 440px) {
|
||||||
|
.hide-large {display: none !important;}
|
||||||
|
.hide-medium {display:none !important}
|
||||||
|
.hide-small {display:none !important}
|
||||||
|
.hide-tiny { display: none !important;}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="w3-row" style="margin-top: 1em; margin-bottom: 1em;">
|
<p>{{.VMID}}</p>
|
||||||
<hr class="w3-show-small w3-hide-medium w3-hide-large" style="margin: 0; margin-bottom: 1em;">
|
<p id="instance-name">{{.Name}}</p>
|
||||||
<p class="w3-col l1 m2 s6">{{.VMID}}</p>
|
<p class="hide-small">{{.Type}}</p>
|
||||||
<p class="w3-col l2 m3 s6" id="instance-name">{{.Name}}</p>
|
<div class="flex row nowrap hide-tiny">
|
||||||
<p class="w3-col l1 m2 w3-hide-small">{{.Type}}</p>
|
{{if eq .Status "running"}}
|
||||||
<div class="w3-col l2 m3 s6 flex row nowrap">
|
<svg id="status" aria-label="instance is running"><use href="images/status/active.svg#symb"></svg>
|
||||||
{{if eq .Status "running"}}
|
{{else if eq .Status "stopped"}}
|
||||||
<svg aria-label="instance is running"><use href="images/status/active.svg#symb"></svg>
|
<svg id="status" aria-label="instance is stopped"><use href="images/status/inactive.svg#symb"></svg>
|
||||||
{{else if eq .Status "stopped"}}
|
{{else if eq .Status "loading"}}
|
||||||
<svg aria-label="instance is stopped"><use href="images/status/inactive.svg#symb"></svg>
|
<svg id="status" aria-label="instance is loading"><use href="images/status/loading.svg#symb"></svg>
|
||||||
{{else if eq .Status "loading"}}
|
{{else}}
|
||||||
<svg aria-label="instance is loading"><use href="images/status/loading.svg#symb"></svg>
|
<svg id="status" aria-label="instance is loading"><use href="images/status/loading.svg#symb"></svg>
|
||||||
{{else}}
|
{{end}}
|
||||||
{{end}}
|
<p>{{.Status}}</p>
|
||||||
<p>{{.Status}}</p>
|
|
||||||
</div>
|
|
||||||
<p class="w3-col l2 w3-hide-medium w3-hide-small">{{.Node}}</p>
|
|
||||||
<div class="w3-col l2 w3-hide-medium w3-hide-small flex row nowrap">
|
|
||||||
{{if eq .NodeStatus "online"}}
|
|
||||||
<svg aria-label="node is online"><use href="images/status/active.svg#symb"></svg>
|
|
||||||
{{else if eq .NodeStatus "offline"}}
|
|
||||||
<svg aria-label="node is offline"><use href="images/status/inactive.svg#symb"></svg>
|
|
||||||
{{else if eq .NodeStatus "unknown"}}
|
|
||||||
<svg aria-label="node is offline"><use href="images/status/inactive.svg#symb"></svg>
|
|
||||||
{{else}}
|
|
||||||
{{end}}
|
|
||||||
<p>{{.NodeStatus}}</p>
|
|
||||||
</div>
|
|
||||||
<div class="w3-col l2 m2 s6 flex row nowrap" style="height: 1lh;">
|
|
||||||
{{if and (eq .NodeStatus "online") (eq .Status "running")}}
|
|
||||||
<svg id="power-btn" class="clickable" aria-label="shutdown instance"><use href="images/actions/instance/stop.svg#symb"></svg>
|
|
||||||
<svg id="configure-btn" aria-label=""><use href="images/actions/instance/config-inactive.svg#symb"></svg>
|
|
||||||
<svg id="console-btn" class="clickable" aria-label="open console"><use href="images/actions/instance/console-active.svg#symb"></svg>
|
|
||||||
<svg id="delete-btn" aria-label=""><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
|
|
||||||
{{else if and (eq .NodeStatus "online") (eq .Status "stopped")}}
|
|
||||||
<svg id="power-btn" class="clickable" aria-label="start instance"><use href="images/actions/instance/start.svg#symb"></svg>
|
|
||||||
<svg id="configure-btn" class="clickable" aria-label="change configuration"><use href="images/actions/instance/config-active.svg#symb"></svg>
|
|
||||||
<svg id="console-btn" aria-label=""><use href="images/actions/instance/console-inactive.svg#symb"></svg>
|
|
||||||
<svg id="delete-btn" class="clickable" aria-label="delete instance"><use href="images/actions/instance/delete-active.svg#symb"></svg>
|
|
||||||
{{else if and (eq .NodeStatus "online") (eq .Status "loading")}}
|
|
||||||
<svg id="power-btn" aria-label=""><use href="images/actions/instance/loading.svg#symb"></svg>
|
|
||||||
<svg id="configure-btn" aria-label=""><use href="images/actions/instance/config-inactive.svg#symb"></svg>
|
|
||||||
<svg id="console-btn" aria-label=""><use href="images/actions/instance/console-inactive.svg#symb"></svg>
|
|
||||||
<svg id="delete-btn" aria-label=""><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
|
|
||||||
{{else}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="hide-medium">{{.Node}}</p>
|
||||||
|
<div class="flex row nowrap hide-medium">
|
||||||
|
{{if eq .NodeStatus "online"}}
|
||||||
|
<svg aria-label="node is online"><use href="images/status/active.svg#symb"></svg>
|
||||||
|
{{else if eq .NodeStatus "offline"}}
|
||||||
|
<svg aria-label="node is offline"><use href="images/status/inactive.svg#symb"></svg>
|
||||||
|
{{else if eq .NodeStatus "unknown"}}
|
||||||
|
<svg aria-label="node is offline"><use href="images/status/inactive.svg#symb"></svg>
|
||||||
|
{{else}}
|
||||||
|
{{end}}
|
||||||
|
<p>{{.NodeStatus}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex row nowrap" style="height: 1lh;">
|
||||||
|
{{if and (eq .NodeStatus "online") (eq .Status "running")}}
|
||||||
|
<svg id="power-btn" class="clickable" aria-label="shutdown instance" role="button" tabindex=0><use href="images/actions/instance/stop.svg#symb"></svg>
|
||||||
|
<svg id="configure-btn" aria-disabled="true" role="none"><use href="images/actions/instance/config-inactive.svg#symb"></svg>
|
||||||
|
<svg id="backup-btn" aria-disabled="true" role="none"><use href="images/actions/instance/backup-inactive.svg#symb"></svg>
|
||||||
|
<a href="{{.ConsolePath}}" target="_blank">
|
||||||
|
<svg id="console-btn" class="clickable" aria-label="open console"><use href="images/actions/instance/console-active.svg#symb"></svg>
|
||||||
|
</a>
|
||||||
|
<svg id="delete-btn" aria-disabled="true" role="none"><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
|
||||||
|
{{else if and (eq .NodeStatus "online") (eq .Status "stopped")}}
|
||||||
|
<svg id="power-btn" class="clickable" aria-label="start instance" role="button" tabindex=0><use href="images/actions/instance/start.svg#symb"></svg>
|
||||||
|
<a href="{{.ConfigPath}}">
|
||||||
|
<svg id="configure-btn" class="clickable" aria-label="change configuration"><use href="images/actions/instance/config-active.svg#symb"></svg>
|
||||||
|
</a>
|
||||||
|
<a href="{{.BackupsPath}}">
|
||||||
|
<svg id="backup-btn" class="clickable" aria-label="manage backups"><use href="images/actions/instance/backup-active.svg#symb"></svg>
|
||||||
|
</a>
|
||||||
|
<svg id="console-btn" aria-disabled="true" role="none"><use href="images/actions/instance/console-inactive.svg#symb"></svg>
|
||||||
|
<svg id="delete-btn" class="clickable" aria-label="delete instance" role="button" tabindex=0><use href="images/actions/instance/delete-active.svg#symb"></svg>
|
||||||
|
{{else if and (eq .NodeStatus "online") (eq .Status "loading")}}
|
||||||
|
<svg id="power-btn" aria-disabled="true" role="none"><use href="images/actions/instance/loading.svg#symb"></svg>
|
||||||
|
<svg id="configure-btn" aria-disabled="true" role="none"><use href="images/actions/instance/config-inactive.svg#symb"></svg>
|
||||||
|
<svg id="backup-btn" aria-disabled="true" role="none"><use href="images/actions/instance/backup-inactive.svg#symb"></svg>
|
||||||
|
<svg id="console-btn" aria-disabled="true" role="none"><use href="images/actions/instance/console-inactive.svg#symb"></svg>
|
||||||
|
<svg id="delete-btn" aria-disabled="true" role="none"><use href="images/actions/instance/delete-inactive.svg#symb"></svg>
|
||||||
|
{{else}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<template id="power-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;">
|
||||||
|
{{if eq .Status "running"}}
|
||||||
|
Stop {{.VMID}}
|
||||||
|
{{else if eq .Status "stopped"}}
|
||||||
|
Start {{.VMID}}
|
||||||
|
{{else}}
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
{{if eq .Status "running"}}
|
||||||
|
<p>Are you sure you want to <strong>stop</strong> {{.VMID}}?</p>
|
||||||
|
{{else if eq .Status "stopped"}}
|
||||||
|
<p>Are you sure you want to <strong>start</strong> {{.VMID}}?</p>
|
||||||
|
{{else}}
|
||||||
|
{{end}}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
<template id="delete-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;">
|
||||||
|
Delete {{.VMID}}
|
||||||
|
</p>
|
||||||
|
<div id="body">
|
||||||
|
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
|
||||||
|
<p>Are you sure you want to <strong>delete</strong> {{.VMID}}?</p>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</instance-card>
|
</instance-card>
|
||||||
{{end}}
|
{{end}}
|
@@ -2,7 +2,6 @@
|
|||||||
<resource-chart>
|
<resource-chart>
|
||||||
<template shadowrootmode="open">
|
<template shadowrootmode="open">
|
||||||
<link rel="stylesheet" href="modules/w3.css">
|
<link rel="stylesheet" href="modules/w3.css">
|
||||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
@@ -13,15 +12,13 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
padding: 10px 10px 10px 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
progress {
|
progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 0;
|
border: 0;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
#caption {
|
#caption {
|
||||||
@@ -30,14 +27,23 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
progress::-moz-progress-bar {
|
||||||
|
background: #{{.ColorHex}};
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-bar {
|
||||||
|
background: var(--main-text-color);
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
background: #{{.ColorHex}};
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<progress value="{{.Used}}" max="{{.Max}}"></progress>
|
<progress value="{{.Used}}" max="{{.Max}}" id="resource"></progress>
|
||||||
<p id="caption">
|
<label id="caption" for="resource">
|
||||||
<span>{{.Name}}</span>
|
<span>{{.Name}}</span>
|
||||||
<span>{{printf "%g" .Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
|
<span>{{printf "%g" .Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
|
||||||
</p>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</resource-chart>
|
</resource-chart>
|
||||||
{{end}}-
|
{{end}}
|