Compare commits
1 Commits
33db3c15b8
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 04c4d990c6 |
@@ -1,6 +1,3 @@
|
|||||||
[submodule "proxmoxaas-common-lib"]
|
[submodule "proxmoxaas-common-lib"]
|
||||||
path = proxmoxaas-common-lib
|
path = proxmoxaas-common-lib
|
||||||
url = https://git.tronnet.net/tronnet/proxmoxaas-common-lib
|
url = https://git.tronnet.net/tronnet/proxmoxaas-common-lib
|
||||||
[submodule "WFA-JS"]
|
|
||||||
path = WFA-JS
|
|
||||||
url = https://git.tronnet.net/alu/WFA-JS
|
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
.PHONY: build test clean wfa-js
|
.PHONY: build test clean
|
||||||
|
|
||||||
build: clean wfa-js
|
build: clean
|
||||||
@echo "======================== Building Binary ======================="
|
@echo "======================== Building Binary ======================="
|
||||||
mkdir -p dist
|
|
||||||
# resolve symbolic links in web by copying it into dist/web/
|
# resolve symbolic links in web by copying it into dist/web/
|
||||||
cp -rL web/ dist/web/
|
cp -rL web/ dist/web/
|
||||||
CGO_ENABLED=0 go build -tags release -ldflags="-s -w" -v -o dist/ .
|
CGO_ENABLED=0 go build -ldflags="-s -w" -v -o dist/ .
|
||||||
|
|
||||||
wfa-js:
|
|
||||||
$(MAKE) -C WFA-JS
|
|
||||||
cp -f WFA-JS/dist/* web/modules
|
|
||||||
|
|
||||||
test: clean
|
test: clean
|
||||||
go run .
|
go run .
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ProxmoxAAS Dashboard - Proxmox As A Service User Web Interface
|
# ProxmoxAAS Dashboard - Proxmox As A Service User Web Interface
|
||||||
ProxmoxAAS Dashboard provides users of a proxmox based compute on demand service a simplified UI which gives users power management, console access, and instance configuration utility. It also allows administrators to set resource quotas for users and allows users to configure instances without administrator priviledges.
|
ProxmoxAAS Dashboard provides a simplified UI for users of a proxmox based compute on demand service. It provides users power management, console access, and instance configuration utility. It also allows administrators to set resource quotas for users and allows users to configure instances without administrator priviledges.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Simplified interface for non administrator users
|
- Simplified interface for non administrator users
|
||||||
@@ -13,7 +13,7 @@ ProxmoxAAS Dashboard provides users of a proxmox based compute on demand service
|
|||||||
|
|
||||||
## ProxmoxAAS System Installation Overview
|
## ProxmoxAAS System Installation Overview
|
||||||
|
|
||||||
The ProxmoxAAS project is large and is split into multiple components. There are four required components, the Dashboard, API, Fabric, and access-manager-api. The instalation order should start with the Dashboard and then proceed to the other backend components. This will require some foresight into the setup process.
|
The ProxmoxAAS project is large and is split into multiple components. There are three required components, the Dashboard, API, and Fabric. There is also an optional LDAP component for organizations that want to use LDAP as their authentication backend. The instalation order should start with the Dashboard and then proceed to the other backend components. This will require some foresight into the setup process.
|
||||||
|
|
||||||
The supported setup is to use a reverse proxy to serve both the original Proxmox web interface and ProxmoxAAS components. It is possible other setups can work. Rather than provide specific steps to duplicate a certain setup, the steps included are intended as a guideline of steps required for proper function in most setups. Consequently, the examples provided are only to highlight key settings and do not represent complete working configurations. The instructions also assume you have your own domain name which will substitute `domain.net` in some of the configs.
|
The supported setup is to use a reverse proxy to serve both the original Proxmox web interface and ProxmoxAAS components. It is possible other setups can work. Rather than provide specific steps to duplicate a certain setup, the steps included are intended as a guideline of steps required for proper function in most setups. Consequently, the examples provided are only to highlight key settings and do not represent complete working configurations. The instructions also assume you have your own domain name which will substitute `domain.net` in some of the configs.
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ We will assume different hosts for each component which are accessible by unique
|
|||||||
| ProxmoxAAS-Dashboard | dashboard.local | paas.domain.net |
|
| ProxmoxAAS-Dashboard | dashboard.local | paas.domain.net |
|
||||||
| ProxmoxAAS-API | api.local | paas.domain.net/api/ |
|
| ProxmoxAAS-API | api.local | paas.domain.net/api/ |
|
||||||
| ProxmoxAAS-Fabric | fabric.local | N/A |
|
| ProxmoxAAS-Fabric | fabric.local | N/A |
|
||||||
| access-manager-api | access.local | N/A|
|
| ProxmoxAAS-LDAP | ldap.local | N/A|
|
||||||
|
|
||||||
## Prerequisites - Dashboard
|
## Prerequisites - Dashboard
|
||||||
- Proxmox VE Cluster (v7.0+)
|
- Proxmox VE Cluster (v7.0+)
|
||||||
@@ -36,11 +36,11 @@ We will assume different hosts for each component which are accessible by unique
|
|||||||
1. Initialize any host, which will be the `ProxmoxAAS-Dashboard` component host
|
1. Initialize any host, which will be the `ProxmoxAAS-Dashboard` component host
|
||||||
2. Download `proxmoxaas-dashboard` binary and `template.config.json` file from [releases](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP/releases)
|
2. Download `proxmoxaas-dashboard` binary and `template.config.json` file from [releases](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP/releases)
|
||||||
Rename `template.config.json` to `config.json` and modify:
|
Rename `template.config.json` to `config.json` and modify:
|
||||||
- `listenPort`: port for PAAS-Dashboard to bind and listen on
|
- listenPort: port for PAAS-Dashboard to bind and listen on
|
||||||
- `organization`: name of your org which is displayed on the top left corner
|
- organization: name of your org which is displayed on the top left corner
|
||||||
- `dashurl`: url for the dashboard, ie. `https://paas.domain.net`
|
- dashurl: url for the dashboard, ie. `https://paas.domain.net`
|
||||||
- `apiurl`: url for PAAS-API, ie. `https://paas.domain.net/api`
|
- apiurl: url for PAAS-API, ie. `https://paas.domain.net/api`
|
||||||
- `pveurl`: url for the Proxmox endpoint, ie. `https://pve.domain.net`
|
- pveurl: url for the Proxmox endpoint, ie. `https://pve.domain.net`
|
||||||
3. Execute the binary or additionally download `proxmoxaas-dashboard.service` from [releases](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP/releases) to run using systemd
|
3. Execute the binary or additionally download `proxmoxaas-dashboard.service` from [releases](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP/releases) to run using systemd
|
||||||
|
|
||||||
After this step, the Dashboard should be available on the `ProxmoxAAS-Dashboard` host at the configured `listenPort`
|
After this step, the Dashboard should be available on the `ProxmoxAAS-Dashboard` host at the configured `listenPort`
|
||||||
@@ -53,9 +53,9 @@ To install the API component, go to [ProxmoxAAS-API](https://git.tronnet.net/tro
|
|||||||
|
|
||||||
To install the Fabric component, go to [ProxmoxAAS-Fabric](https://git.tronnet.net/tronnet/ProxmoxAAS-Fabric). This is required for the app to function. The Fabric installation will also have steps for setting up the reverse proxy server.
|
To install the Fabric component, go to [ProxmoxAAS-Fabric](https://git.tronnet.net/tronnet/ProxmoxAAS-Fabric). This is required for the app to function. The Fabric installation will also have steps for setting up the reverse proxy server.
|
||||||
|
|
||||||
## Installation - access-manager-api
|
## Installation - LDAP
|
||||||
|
|
||||||
To install the access-manager-api component, go to [access-manager-api](https://git.tronnet.net/tronnet/access-manager-api). This is required for the app to function. The access-manager-api installation will also have steps for setting up the reverse proxy server.
|
To install the LDAP component, go to [ProxmoxAAS-LDAP](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP).This is an optional component which adds a lightweight REST API server ontop of a simplified LDAP environment. It is only used by the API as a potential authentication backend.
|
||||||
|
|
||||||
## Installation - Reverse Proxy
|
## Installation - Reverse Proxy
|
||||||
1. Configure nginx or preferred reverse proxy to reverse proxy the dashboard. The configuration should include at least the following, ensuring that the configured ports are adjusted appropriately:
|
1. Configure nginx or preferred reverse proxy to reverse proxy the dashboard. The configuration should include at least the following, ensuring that the configured ports are adjusted appropriately:
|
||||||
|
|||||||
-1
Submodule WFA-JS deleted from f8f8636cc3
+2
-6
@@ -1,7 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
@@ -12,10 +11,7 @@ import (
|
|||||||
"github.com/tdewolff/minify/v2"
|
"github.com/tdewolff/minify/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run() {
|
func Run(configPath *string) {
|
||||||
configPath := flag.String("config", "config.json", "path to config.json file")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
common.Global = common.GetConfig(*configPath)
|
common.Global = common.GetConfig(*configPath)
|
||||||
|
|
||||||
// setup static resources
|
// setup static resources
|
||||||
@@ -42,7 +38,7 @@ func Run() {
|
|||||||
router.GET("/settings", routes.HandleGETSettings)
|
router.GET("/settings", routes.HandleGETSettings)
|
||||||
|
|
||||||
// run on all interfaces with port
|
// run on all interfaces with port
|
||||||
log.Fatal("[ERR ] starting gin router: ", router.Run(fmt.Sprintf("0.0.0.0:%d", common.Global.Port)))
|
log.Fatal(router.Run(fmt.Sprintf("0.0.0.0:%d", common.Global.Port)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup static resources under web (css, images, modules, scripts)
|
// setup static resources under web (css, images, modules, scripts)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build release
|
|
||||||
|
|
||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -57,3 +55,45 @@ 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,
|
||||||
|
},
|
||||||
|
"png": {
|
||||||
|
Type: "image/png",
|
||||||
|
Minifier: nil,
|
||||||
|
},
|
||||||
|
"js": {
|
||||||
|
Type: "application/javascript",
|
||||||
|
Minifier: nil,
|
||||||
|
},
|
||||||
|
"wasm": {
|
||||||
|
Type: "application/wasm",
|
||||||
|
Minifier: nil,
|
||||||
|
},
|
||||||
|
"*": {
|
||||||
|
Type: "text/plain",
|
||||||
|
Minifier: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
*/
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//go:build !release
|
|
||||||
|
|
||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/tdewolff/minify/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// defines mime type and associated minifier
|
|
||||||
type MimeType struct {
|
|
||||||
Type string
|
|
||||||
Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// debug mime types
|
|
||||||
var MimeTypes = map[string]MimeType{
|
|
||||||
"css": {
|
|
||||||
Type: "text/css",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
"html": {
|
|
||||||
Type: "text/html",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
"tmpl": {
|
|
||||||
Type: "text/plain",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
"frag": {
|
|
||||||
Type: "text/plain",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
"svg": {
|
|
||||||
Type: "image/svg+xml",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
"png": {
|
|
||||||
Type: "image/png",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
"js": {
|
|
||||||
Type: "application/javascript",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
"wasm": {
|
|
||||||
Type: "application/wasm",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
"*": {
|
|
||||||
Type: "text/plain",
|
|
||||||
Minifier: nil,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,13 @@ type StaticFile struct {
|
|||||||
MimeType MimeType
|
MimeType MimeType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsed vmpath data (ie node/type/vmid)
|
||||||
|
type VMPath struct {
|
||||||
|
Node string
|
||||||
|
Type string
|
||||||
|
VMID string
|
||||||
|
}
|
||||||
|
|
||||||
// type used for templated <select>
|
// type used for templated <select>
|
||||||
type Select struct {
|
type Select struct {
|
||||||
ID string
|
ID string
|
||||||
@@ -35,3 +42,20 @@ type Option struct {
|
|||||||
Value string
|
Value string
|
||||||
Display string
|
Display string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RequestContext struct {
|
||||||
|
Cookies map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
Username string
|
||||||
|
Token string
|
||||||
|
CSRF string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Icon struct {
|
||||||
|
ID string
|
||||||
|
Src string
|
||||||
|
Alt string
|
||||||
|
Clickable bool
|
||||||
|
}
|
||||||
|
|||||||
+45
-59
@@ -10,37 +10,27 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/tdewolff/minify/v2"
|
"github.com/tdewolff/minify/v2"
|
||||||
|
|
||||||
paas "proxmoxaas-common-lib"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// get config file from configPath
|
// get config file from configPath
|
||||||
func GetConfig(configPath string) Config {
|
func GetConfig(configPath string) Config {
|
||||||
root, err := os.OpenRoot(".")
|
content, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("[ERR ] error opening root dir: ", err)
|
log.Fatal("Error when opening config file: ", err)
|
||||||
}
|
}
|
||||||
defer root.Close()
|
|
||||||
|
|
||||||
content, err := root.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("[ERR ] error opening config file: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
err = json.Unmarshal(content, &config)
|
err = json.Unmarshal(content, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("[ERR ] error parsing config file: ", err)
|
log.Fatal("Error during parsing config file: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,14 +47,14 @@ func InitMinify() *minify.M {
|
|||||||
|
|
||||||
func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile {
|
func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile {
|
||||||
minified := make(map[string]StaticFile)
|
minified := make(map[string]StaticFile)
|
||||||
err := fs.WalkDir(files, ".", func(path string, entry fs.DirEntry, err error) error {
|
fs.WalkDir(files, ".", func(path string, entry fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
v, err := files.ReadFile(path)
|
v, err := files.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[ERR ] error parsing template file %s: %s", path, err.Error())
|
log.Fatalf("error parsing template file %s: %s", path, err.Error())
|
||||||
}
|
}
|
||||||
x := strings.Split(entry.Name(), ".")
|
x := strings.Split(entry.Name(), ".")
|
||||||
if len(x) >= 2 { // file has extension
|
if len(x) >= 2 { // file has extension
|
||||||
@@ -72,7 +62,7 @@ func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile {
|
|||||||
if ok && mimetype.Minifier != nil { // if the extension is mapped in MimeTypes and has a minifier
|
if ok && mimetype.Minifier != nil { // if the extension is mapped in MimeTypes and has a minifier
|
||||||
min, err := m.String(mimetype.Type, string(v)) // try to minify
|
min, err := m.String(mimetype.Type, string(v)) // try to minify
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[ERR ] error minifying file %s: %s", path, err.Error())
|
log.Fatalf("error minifying file %s: %s", path, err.Error())
|
||||||
}
|
}
|
||||||
minified[path] = StaticFile{
|
minified[path] = StaticFile{
|
||||||
Data: min,
|
Data: min,
|
||||||
@@ -94,13 +84,7 @@ func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[ERR ] error in MinifyStatic: %s", err)
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return minified
|
return minified
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadHTMLToGin(engine *gin.Engine, html map[string]StaticFile) *template.Template {
|
func LoadHTMLToGin(engine *gin.Engine, html map[string]StaticFile) *template.Template {
|
||||||
@@ -109,20 +93,20 @@ func LoadHTMLToGin(engine *gin.Engine, html map[string]StaticFile) *template.Tem
|
|||||||
"MapKeys": func(x any, sep string) string {
|
"MapKeys": func(x any, sep string) string {
|
||||||
v := reflect.ValueOf(x)
|
v := reflect.ValueOf(x)
|
||||||
keys := v.MapKeys()
|
keys := v.MapKeys()
|
||||||
var s strings.Builder
|
s := ""
|
||||||
for i := range keys {
|
for i := 0; i < len(keys); i++ {
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
s.WriteString(sep)
|
s += sep
|
||||||
}
|
}
|
||||||
s.WriteString(keys[i].String())
|
s += keys[i].String()
|
||||||
}
|
}
|
||||||
return s.String()
|
return s
|
||||||
},
|
},
|
||||||
"Map": func(values ...any) (map[string]any, error) {
|
"Map": func(values ...any) (map[string]any, error) {
|
||||||
if len(values)%2 != 0 {
|
if len(values)%2 != 0 {
|
||||||
return nil, errors.New("invalid dict call")
|
return nil, errors.New("invalid dict call")
|
||||||
}
|
}
|
||||||
dict := make(map[string]any, len(values)/2)
|
dict := make(map[string]interface{}, len(values)/2)
|
||||||
for i := 0; i < len(values); i += 2 {
|
for i := 0; i < len(values); i += 2 {
|
||||||
key, ok := values[i].(string)
|
key, ok := values[i].(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -171,20 +155,17 @@ func TemplateMinifier(m *minify.M, w io.Writer, r io.Reader, _ map[string]string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleNonFatalError(c *gin.Context, err error) {
|
func HandleNonFatalError(c *gin.Context, err error) {
|
||||||
log.Printf("[WARN] encountered a non-fatal error: %s", err.Error())
|
log.Printf("[Error] encountered an error: %s", err.Error())
|
||||||
c.Status(http.StatusInternalServerError)
|
c.Status(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RequestGetAPI(path string, auth *paas.Auth, body any) (*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
|
||||||
}
|
}
|
||||||
|
for k, v := range context.Cookies {
|
||||||
if auth != nil {
|
req.AddCookie(&http.Cookie{Name: k, Value: v})
|
||||||
for k, v := range GetRequestContextFromCookies(*auth) {
|
|
||||||
req.AddCookie(&http.Cookie{Name: k, Value: v, Secure: true})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
@@ -204,6 +185,7 @@ func RequestGetAPI(path string, auth *paas.Auth, body any) (*http.Response, int,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.StatusCode, err
|
return nil, response.StatusCode, err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch body.(type) { // write json to body object depending on type, currently supports map[string]any (ie json) or []any (ie array of json)
|
switch body.(type) { // write json to body object depending on type, currently supports map[string]any (ie json) or []any (ie array of json)
|
||||||
case *map[string]any:
|
case *map[string]any:
|
||||||
err = json.Unmarshal(data, &body)
|
err = json.Unmarshal(data, &body)
|
||||||
@@ -221,45 +203,49 @@ func RequestGetAPI(path string, auth *paas.Auth, body any) (*http.Response, int,
|
|||||||
return response, response.StatusCode, nil
|
return response, response.StatusCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAuthFromRequest(c *gin.Context) (paas.Auth, error) {
|
func GetAuth(c *gin.Context) (Auth, error) {
|
||||||
_, errAuth := c.Cookie("auth")
|
_, errAuth := c.Cookie("auth")
|
||||||
username, errUsername := c.Cookie("username")
|
username, errUsername := c.Cookie("username")
|
||||||
token, errToken := c.Cookie("PVEAuthCookie")
|
token, errToken := c.Cookie("PVEAuthCookie")
|
||||||
csrf, errCSRF := c.Cookie("CSRFPreventionToken")
|
csrf, errCSRF := c.Cookie("CSRFPreventionToken")
|
||||||
access, errAccess := c.Cookie("PAASAccessManagerTicket")
|
if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil {
|
||||||
if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil || errAccess != nil {
|
return Auth{}, fmt.Errorf("error occured getting user cookies: (auth: %s, token: %s, csrf: %s)", errAuth, errToken, errCSRF)
|
||||||
return paas.Auth{}, fmt.Errorf("error occured getting user cookies: (auth: %s, token: %s, csrf: %s)", errAuth, errToken, errCSRF)
|
|
||||||
} else {
|
} else {
|
||||||
return paas.Auth{Username: username, Token: token, CSRF: csrf, AccessManagerTicket: access}, nil
|
return Auth{username, token, csrf}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInstancePathFromRequest(c *gin.Context) (paas.InstancePath, error) {
|
func ExtractVMPath(c *gin.Context) (VMPath, error) {
|
||||||
req_node := c.Query("node")
|
req_node := c.Query("node")
|
||||||
req_type := c.Query("type")
|
req_type := c.Query("type")
|
||||||
req_vmid := c.Query("vmid")
|
req_vmid := c.Query("vmid")
|
||||||
if req_node == "" || req_type == "" || req_vmid == "" {
|
if req_node == "" || req_type == "" || req_vmid == "" {
|
||||||
return paas.InstancePath{}, fmt.Errorf("request missing required values: (node: %s, type: %s, vmid: %s)", req_node, req_type, req_vmid)
|
return VMPath{}, fmt.Errorf("request missing required values: (node: %s, type: %s, vmid: %s)", req_node, req_type, req_vmid)
|
||||||
}
|
}
|
||||||
|
vm_path := VMPath{
|
||||||
vmid_int, err := strconv.ParseUint(req_vmid, 10, 64)
|
Node: req_node,
|
||||||
if err != nil {
|
Type: req_type,
|
||||||
return paas.InstancePath{}, err
|
VMID: req_vmid,
|
||||||
}
|
|
||||||
|
|
||||||
vm_path := paas.InstancePath{
|
|
||||||
NodeName: req_node,
|
|
||||||
InstanceType: paas.InstanceType(req_type),
|
|
||||||
InstanceID: paas.InstanceID(vmid_int),
|
|
||||||
}
|
}
|
||||||
return vm_path, nil
|
return vm_path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRequestContextFromCookies(auth paas.Auth) map[string]string {
|
func FormatNumber(val int64, base int64) (float64, string) {
|
||||||
return map[string]string{
|
valf := float64(val)
|
||||||
"username": auth.Username,
|
basef := float64(base)
|
||||||
"PVEAuthCookie": auth.Token,
|
steps := 0
|
||||||
"CSRFPreventionToken": auth.CSRF,
|
for math.Abs(valf) > basef && steps < 4 {
|
||||||
"PAASAccessManagerTicket": auth.AccessManagerTicket,
|
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, ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-81
@@ -3,7 +3,6 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
paas "proxmoxaas-common-lib"
|
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
|
|
||||||
"github.com/gerow/go-color"
|
"github.com/gerow/go-color"
|
||||||
@@ -12,8 +11,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
paas.User
|
Username string
|
||||||
Pools map[string]paas.Pool
|
Pools map[string]bool
|
||||||
|
Nodes map[string]bool
|
||||||
|
VMID struct {
|
||||||
|
Min int
|
||||||
|
Max int
|
||||||
|
}
|
||||||
|
Resources map[string]map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
// numerical constraint
|
// numerical constraint
|
||||||
@@ -36,7 +41,7 @@ type NumericResource struct {
|
|||||||
Type string
|
Type string
|
||||||
Name string
|
Name string
|
||||||
Multiplier int64
|
Multiplier int64
|
||||||
Base uint64
|
Base int64
|
||||||
Compact bool
|
Compact bool
|
||||||
Unit string
|
Unit string
|
||||||
Display bool
|
Display bool
|
||||||
@@ -50,7 +55,7 @@ type StorageResource struct {
|
|||||||
Type string
|
Type string
|
||||||
Name string
|
Name string
|
||||||
Multiplier int64
|
Multiplier int64
|
||||||
Base uint64
|
Base int64
|
||||||
Compact bool
|
Compact bool
|
||||||
Unit string
|
Unit string
|
||||||
Display bool
|
Display bool
|
||||||
@@ -77,7 +82,7 @@ type ResourceChart struct {
|
|||||||
Name string
|
Name string
|
||||||
Used int64
|
Used int64
|
||||||
Max int64
|
Max int64
|
||||||
Avail string
|
Avail float64
|
||||||
Prefix string
|
Prefix string
|
||||||
Unit string
|
Unit string
|
||||||
ColorHex string
|
ColorHex string
|
||||||
@@ -96,31 +101,21 @@ var Green = color.RGB{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETAccount(c *gin.Context) {
|
func HandleGETAccount(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
account, err := GetUserAccount(auth)
|
||||||
account, err := GetUser(auth)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pools, err := GetUserPools(auth)
|
// for each resource category, create a resource chart
|
||||||
if err != nil {
|
for category, resources := range account.Resources {
|
||||||
common.HandleNonFatalError(c, err)
|
for resource, v := range resources {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for poolname, pool := range pools {
|
|
||||||
// for each resource category
|
|
||||||
for category := range pool.Resources {
|
|
||||||
// for each resource in each category
|
|
||||||
for resource, v := range pool.Resources[category].(map[string]any) {
|
|
||||||
// create a resource chart for resource depending on resource type
|
|
||||||
switch t := v.(type) {
|
switch t := v.(type) {
|
||||||
case NumericResource:
|
case NumericResource:
|
||||||
avail, prefix := paas.FormatNumber(paas.SafeUint64(t.Total.Avail*t.Multiplier), t.Base)
|
avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
||||||
pools[poolname].Resources[category].(map[string]any)[resource] = ResourceChart{
|
account.Resources[category][resource] = ResourceChart{
|
||||||
Type: t.Type,
|
Type: t.Type,
|
||||||
Display: t.Display,
|
Display: t.Display,
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
@@ -132,8 +127,8 @@ func HandleGETAccount(c *gin.Context) {
|
|||||||
ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(),
|
ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(),
|
||||||
}
|
}
|
||||||
case StorageResource:
|
case StorageResource:
|
||||||
avail, prefix := paas.FormatNumber(paas.SafeUint64(t.Total.Avail*t.Multiplier), t.Base)
|
avail, prefix := common.FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
|
||||||
pools[poolname].Resources[category].(map[string]any)[resource] = ResourceChart{
|
account.Resources[category][resource] = ResourceChart{
|
||||||
Type: t.Type,
|
Type: t.Type,
|
||||||
Display: t.Display,
|
Display: t.Display,
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
@@ -156,25 +151,21 @@ func HandleGETAccount(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range t.Total {
|
for _, r := range t.Total {
|
||||||
avail := fmt.Sprintf("%d", r.Avail)
|
|
||||||
l.Resources = append(l.Resources, ResourceChart{
|
l.Resources = append(l.Resources, ResourceChart{
|
||||||
Type: t.Type,
|
Type: t.Type,
|
||||||
Display: t.Display,
|
Display: t.Display,
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
Used: r.Used,
|
Used: r.Used,
|
||||||
Max: r.Max,
|
Max: r.Max,
|
||||||
Avail: avail, // usually an int
|
Avail: float64(r.Avail), // usually an int
|
||||||
Unit: "",
|
Unit: "",
|
||||||
ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(),
|
ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pools[poolname].Resources[category].(map[string]any)[resource] = l
|
account.Resources[category][resource] = l
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
account.Pools = pools
|
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "html/account.html", gin.H{
|
c.HTML(http.StatusOK, "html/account.html", gin.H{
|
||||||
"global": common.Global,
|
"global": common.Global,
|
||||||
@@ -186,100 +177,97 @@ func HandleGETAccount(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUser(auth paas.Auth) (Account, error) {
|
func GetUserAccount(auth common.Auth) (Account, error) {
|
||||||
account := Account{}
|
account := Account{
|
||||||
|
Resources: map[string]map[string]any{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := common.RequestContext{
|
||||||
|
Cookies: map[string]string{
|
||||||
|
"username": auth.Username,
|
||||||
|
"PVEAuthCookie": auth.Token,
|
||||||
|
"CSRFPreventionToken": auth.CSRF,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// get user account basic data
|
||||||
body := map[string]any{}
|
body := map[string]any{}
|
||||||
res, code, err := common.RequestGetAPI(fmt.Sprintf("/access/users/%s", auth.Username), &auth, &body)
|
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 /access/pools resulted in %+v", res)
|
return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res)
|
||||||
}
|
}
|
||||||
err = mapstructure.Decode(body, &account)
|
err = mapstructure.Decode(body, &account)
|
||||||
return account, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetUserPools(auth paas.Auth) (map[string]paas.Pool, error) {
|
|
||||||
pools := map[string]paas.Pool{}
|
|
||||||
|
|
||||||
// get all pools
|
|
||||||
body := map[string]any{}
|
|
||||||
res, code, err := common.RequestGetAPI("/access/pools", &auth, &body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pools, err
|
return account, err
|
||||||
|
} else {
|
||||||
|
account.Username = auth.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
body = map[string]any{}
|
||||||
|
// get user resources
|
||||||
|
res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx, &body)
|
||||||
|
if err != nil {
|
||||||
|
return account, err
|
||||||
}
|
}
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
return pools, fmt.Errorf("request to /access/pools resulted in %+v", res)
|
return account, fmt.Errorf("request to /user/dynamic/resources resulted in %+v", res)
|
||||||
}
|
|
||||||
err = mapstructure.Decode(body["pools"].(map[string]any), &pools)
|
|
||||||
if err != nil {
|
|
||||||
return pools, err
|
|
||||||
}
|
}
|
||||||
|
resources := body
|
||||||
|
|
||||||
// get global config for resource type metadata
|
|
||||||
body = map[string]any{}
|
body = map[string]any{}
|
||||||
// get resource meta data
|
// get resource meta data
|
||||||
res, code, err = common.RequestGetAPI("/global/config/resources", &auth, &body)
|
res, code, err = common.RequestGetAPI("/global/config/resources", ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pools, err
|
return account, err
|
||||||
}
|
}
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
return pools, 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 := body["resources"].(map[string]any)
|
meta := body["resources"].(map[string]any)
|
||||||
|
|
||||||
// for each pool
|
// build each resource by its meta type
|
||||||
for poolname, pool := range pools {
|
for k, v := range meta {
|
||||||
// for each resource in pool data
|
m := v.(map[string]any)
|
||||||
for k, v := range pool.Resources {
|
|
||||||
m := meta[k].(map[string]any)
|
|
||||||
t := m["type"].(string)
|
t := m["type"].(string)
|
||||||
r := v.(map[string]any)
|
r := resources[k].(map[string]any)
|
||||||
category := m["category"].(string)
|
category := m["category"].(string)
|
||||||
|
if _, ok := account.Resources[category]; !ok {
|
||||||
// create a category if it does not already exist
|
account.Resources[category] = map[string]any{}
|
||||||
if _, ok := pool.Resources[category]; !ok {
|
|
||||||
pool.Resources[category] = map[string]any{}
|
|
||||||
}
|
}
|
||||||
|
if t == "numeric" {
|
||||||
// depending on type, decode the pool data into the corresponding resource type
|
|
||||||
switch t {
|
|
||||||
case "numeric":
|
|
||||||
n := NumericResource{}
|
n := NumericResource{}
|
||||||
n.Type = t
|
n.Type = t
|
||||||
err_m := mapstructure.Decode(m, &n)
|
err_m := mapstructure.Decode(m, &n)
|
||||||
err_r := mapstructure.Decode(r, &n)
|
err_r := mapstructure.Decode(r, &n)
|
||||||
if err_m != nil || err_r != nil {
|
if err_m != nil || err_r != nil {
|
||||||
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
}
|
}
|
||||||
pools[poolname].Resources[category].(map[string]any)[k] = n
|
account.Resources[category][k] = n
|
||||||
case "storage":
|
} else if t == "storage" {
|
||||||
n := StorageResource{}
|
n := StorageResource{}
|
||||||
n.Type = t
|
n.Type = t
|
||||||
err_m := mapstructure.Decode(m, &n)
|
err_m := mapstructure.Decode(m, &n)
|
||||||
err_r := mapstructure.Decode(r, &n)
|
err_r := mapstructure.Decode(r, &n)
|
||||||
if err_m != nil || err_r != nil {
|
if err_m != nil || err_r != nil {
|
||||||
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
}
|
}
|
||||||
pools[poolname].Resources[category].(map[string]any)[k] = n
|
account.Resources[category][k] = n
|
||||||
case "list":
|
} else if t == "list" {
|
||||||
n := ListResource{}
|
n := ListResource{}
|
||||||
n.Type = t
|
n.Type = t
|
||||||
err_m := mapstructure.Decode(m, &n)
|
err_m := mapstructure.Decode(m, &n)
|
||||||
err_r := mapstructure.Decode(r, &n)
|
err_r := mapstructure.Decode(r, &n)
|
||||||
if err_m != nil || err_r != nil {
|
if err_m != nil || err_r != nil {
|
||||||
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
|
||||||
}
|
}
|
||||||
pools[poolname].Resources[category].(map[string]any)[k] = n
|
account.Resources[category][k] = n
|
||||||
}
|
|
||||||
|
|
||||||
// delete the old entry, only categories should be left at the end of the loop
|
|
||||||
delete(pools[poolname].Resources, k)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pools, nil
|
return account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// interpolate between min and max by normalized (0 - 1) val
|
// interpolate between min and max by normalized (0 - 1) val
|
||||||
|
|||||||
+20
-15
@@ -2,8 +2,8 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
paas "proxmoxaas-common-lib"
|
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ type InstanceBackup struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETBackups(c *gin.Context) {
|
func HandleGETBackups(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
return
|
return
|
||||||
@@ -39,6 +39,8 @@ func HandleGETBackups(c *gin.Context) {
|
|||||||
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error()))
|
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{
|
c.HTML(http.StatusOK, "html/backups.html", gin.H{
|
||||||
"global": common.Global,
|
"global": common.Global,
|
||||||
"page": "backups",
|
"page": "backups",
|
||||||
@@ -51,9 +53,9 @@ func HandleGETBackups(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETBackupsFragment(c *gin.Context) {
|
func HandleGETBackupsFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil { // user should be authed, try to return index with population
|
if err == nil { // user should be authed, try to return index with population
|
||||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
return
|
return
|
||||||
@@ -65,24 +67,27 @@ func HandleGETBackupsFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/backups-backups.go.tmpl", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/backups-backups.go.tmpl", gin.H{
|
||||||
"backups": backups,
|
"backups": backups,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
c.Status(http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
|
||||||
} else { // return 401
|
} else { // return 401
|
||||||
c.Status(http.StatusUnauthorized)
|
c.Status(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInstanceBackups(vm paas.InstancePath, auth paas.Auth) ([]InstanceBackup, error) {
|
func GetInstanceBackups(vm common.VMPath, auth common.Auth) ([]InstanceBackup, error) {
|
||||||
backups := []InstanceBackup{}
|
backups := []InstanceBackup{}
|
||||||
path := fmt.Sprintf("/cluster/%s/%s/%d/backup", vm.NodeName, string(vm.InstanceType), uint64(vm.InstanceID))
|
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{}
|
body := []any{}
|
||||||
res, code, err := common.RequestGetAPI(path, &auth, &body)
|
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return backups, err
|
return backups, err
|
||||||
}
|
}
|
||||||
@@ -96,8 +101,8 @@ func GetInstanceBackups(vm paas.InstancePath, auth paas.Auth) ([]InstanceBackup,
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range backups {
|
for i := range backups {
|
||||||
size, prefix := paas.FormatNumber(paas.SafeUint64(backups[i].Size), paas.Base1024)
|
size, prefix := common.FormatNumber(backups[i].Size, 1024)
|
||||||
backups[i].SizeFormatted = fmt.Sprintf("%s %sB", size, prefix)
|
backups[i].SizeFormatted = fmt.Sprintf("%.3g %sB", size, prefix)
|
||||||
|
|
||||||
t := time.Unix(backups[i].CTime, 0)
|
t := time.Unix(backups[i].CTime, 0)
|
||||||
backups[i].TimeFormatted = t.Format("02-01-06 15:04:05")
|
backups[i].TimeFormatted = t.Format("02-01-06 15:04:05")
|
||||||
|
|||||||
+68
-57
@@ -15,7 +15,16 @@ import (
|
|||||||
// imported types from fabric
|
// imported types from fabric
|
||||||
|
|
||||||
type InstanceConfig struct {
|
type InstanceConfig struct {
|
||||||
paas.Instance `mapstructure:",squash"`
|
Type paas.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]*paas.Volume `json:"volumes"`
|
||||||
|
Nets map[string]*paas.Net `json:"nets"`
|
||||||
|
Devices map[string]*paas.Device `json:"devices"`
|
||||||
|
Boot paas.BootOrder `json:"boot"`
|
||||||
// overrides
|
// overrides
|
||||||
ProctypeSelect common.Select
|
ProctypeSelect common.Select
|
||||||
}
|
}
|
||||||
@@ -26,17 +35,21 @@ type GlobalConfig struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PoolConfig struct {
|
type UserConfigResources struct {
|
||||||
CPU struct {
|
CPU struct {
|
||||||
Global []paas.MatchLimit
|
Global []CPUConfig
|
||||||
Nodes map[string][]paas.MatchLimit
|
Nodes map[string][]CPUConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CPUConfig struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
func HandleGETConfig(c *gin.Context) {
|
func HandleGETConfig(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
return
|
return
|
||||||
@@ -48,13 +61,13 @@ func HandleGETConfig(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.Type == "VM" { // if VM, fetch CPU types from node
|
if config.Type == "VM" { // if VM, fetch CPU types from node
|
||||||
config.ProctypeSelect, err = GetCPUTypes(vm_path, config.Pool, auth)
|
config.ProctypeSelect, err = GetCPUTypes(vm_path, auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting proctypes: %s", err.Error()))
|
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting proctypes: %s", err.Error()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,9 +83,9 @@ func HandleGETConfig(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETConfigVolumesFragment(c *gin.Context) {
|
func HandleGETConfigVolumesFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
return
|
return
|
||||||
@@ -84,23 +97,19 @@ func HandleGETConfigVolumesFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-volumes.go.tmpl", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/config-volumes.go.tmpl", gin.H{
|
||||||
"config": config,
|
"config": config,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
c.Status(http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusUnauthorized)
|
c.Status(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETConfigNetsFragment(c *gin.Context) {
|
func HandleGETConfigNetsFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
return
|
return
|
||||||
@@ -112,23 +121,19 @@ func HandleGETConfigNetsFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-nets.go.tmpl", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/config-nets.go.tmpl", gin.H{
|
||||||
"config": config,
|
"config": config,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
c.Status(http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusUnauthorized)
|
c.Status(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETConfigDevicesFragment(c *gin.Context) {
|
func HandleGETConfigDevicesFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
return
|
return
|
||||||
@@ -140,23 +145,19 @@ func HandleGETConfigDevicesFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-devices.go.tmpl", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/config-devices.go.tmpl", gin.H{
|
||||||
"config": config,
|
"config": config,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
c.Status(http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusUnauthorized)
|
c.Status(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETConfigBootFragment(c *gin.Context) {
|
func HandleGETConfigBootFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
vm_path, err := common.GetInstancePathFromRequest(c)
|
vm_path, err := common.ExtractVMPath(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.HandleNonFatalError(c, err)
|
common.HandleNonFatalError(c, err)
|
||||||
return
|
return
|
||||||
@@ -168,24 +169,27 @@ func HandleGETConfigBootFragment(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-boot.go.tmpl", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/config-boot.go.tmpl", gin.H{
|
||||||
"config": config,
|
"config": config,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
c.Status(http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
c.Status(http.StatusUnauthorized)
|
c.Status(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInstanceConfig(vm paas.InstancePath, auth paas.Auth) (InstanceConfig, error) {
|
func GetInstanceConfig(vm common.VMPath, auth common.Auth) (InstanceConfig, error) {
|
||||||
config := InstanceConfig{}
|
config := InstanceConfig{}
|
||||||
path := fmt.Sprintf("/cluster/%s/%s/%d/", vm.NodeName, string(vm.InstanceType), uint64(vm.InstanceID))
|
path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID)
|
||||||
|
ctx := common.RequestContext{
|
||||||
|
Cookies: map[string]string{
|
||||||
|
"username": auth.Username,
|
||||||
|
"PVEAuthCookie": auth.Token,
|
||||||
|
"CSRFPreventionToken": auth.CSRF,
|
||||||
|
},
|
||||||
|
}
|
||||||
body := map[string]any{}
|
body := map[string]any{}
|
||||||
res, code, err := common.RequestGetAPI(path, &auth, &body)
|
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return config, err
|
return config, err
|
||||||
}
|
}
|
||||||
@@ -204,53 +208,60 @@ func GetInstanceConfig(vm paas.InstancePath, auth paas.Auth) (InstanceConfig, er
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Select, error) {
|
func GetCPUTypes(vm common.VMPath, auth common.Auth) (common.Select, error) {
|
||||||
cputypes := common.Select{
|
cputypes := common.Select{
|
||||||
ID: "proctype",
|
ID: "proctype",
|
||||||
Required: true,
|
Required: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// get global resource config
|
// get global resource config
|
||||||
|
ctx := common.RequestContext{
|
||||||
|
Cookies: map[string]string{
|
||||||
|
"username": auth.Username,
|
||||||
|
"PVEAuthCookie": auth.Token,
|
||||||
|
"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, &auth, &body)
|
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)
|
||||||
}
|
}
|
||||||
globalConfig := GlobalConfig{}
|
global := GlobalConfig{}
|
||||||
err = mapstructure.Decode(body["resources"], &globalConfig)
|
err = mapstructure.Decode(body["resources"], &global)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// get pool resource config
|
// get user resource config
|
||||||
body = map[string]any{}
|
body = map[string]any{}
|
||||||
path = fmt.Sprintf("/access/pools/%s", pool)
|
path = "/user/config/resources"
|
||||||
res, code, err = common.RequestGetAPI(path, &auth, &body)
|
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)
|
||||||
}
|
}
|
||||||
poolCPUConfig := PoolConfig{}
|
user := UserConfigResources{}
|
||||||
err = mapstructure.Decode(body["pool"].(map[string]any)["resources"], &poolCPUConfig)
|
err = mapstructure.Decode(body, &user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// use node specific rules if present, otherwise use global rules
|
// use node specific rules if present, otherwise use global rules
|
||||||
var userCPU []paas.MatchLimit
|
var userCPU []CPUConfig
|
||||||
if _, ok := poolCPUConfig.CPU.Nodes[vm.NodeName]; ok {
|
if _, ok := user.CPU.Nodes[vm.Node]; ok {
|
||||||
userCPU = poolCPUConfig.CPU.Nodes[vm.NodeName]
|
userCPU = user.CPU.Nodes[vm.Node]
|
||||||
} else {
|
} else {
|
||||||
userCPU = poolCPUConfig.CPU.Global
|
userCPU = user.CPU.Global
|
||||||
}
|
}
|
||||||
|
|
||||||
if globalConfig.CPU.Whitelist { // cpu is a whitelist
|
if global.CPU.Whitelist { // cpu is a whitelist
|
||||||
for _, cpu := range userCPU { // for each cpu type in user config add it to the options
|
for _, cpu := range userCPU { // for each cpu type in user config add it to the options
|
||||||
cputypes.Options = append(cputypes.Options, common.Option{
|
cputypes.Options = append(cputypes.Options, common.Option{
|
||||||
Display: cpu.Name,
|
Display: cpu.Name,
|
||||||
@@ -260,8 +271,8 @@ func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Sele
|
|||||||
} 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
|
||||||
body = map[string]any{}
|
body = map[string]any{}
|
||||||
path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.NodeName)
|
path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.Node)
|
||||||
res, code, err = common.RequestGetAPI(path, &auth, &body)
|
res, code, err = common.RequestGetAPI(path, ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cputypes, err
|
return cputypes, err
|
||||||
}
|
}
|
||||||
@@ -269,7 +280,7 @@ func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Sele
|
|||||||
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||||
}
|
}
|
||||||
supported := struct {
|
supported := struct {
|
||||||
data []paas.MatchLimit
|
data []CPUConfig
|
||||||
}{}
|
}{}
|
||||||
err = mapstructure.Decode(body, supported)
|
err = mapstructure.Decode(body, supported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -278,7 +289,7 @@ func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Sele
|
|||||||
|
|
||||||
// for each node supported cpu type, if it is NOT in the user's config (aka is not blacklisted) then add it to the options
|
// for each node supported cpu type, if it is NOT in the user's config (aka is not blacklisted) then add it to the options
|
||||||
for _, cpu := range supported.data {
|
for _, cpu := range supported.data {
|
||||||
contains := slices.ContainsFunc(userCPU, func(c paas.MatchLimit) bool {
|
contains := slices.ContainsFunc(userCPU, func(c CPUConfig) bool {
|
||||||
return c.Name == cpu.Name
|
return c.Name == cpu.Name
|
||||||
})
|
})
|
||||||
if !contains {
|
if !contains {
|
||||||
|
|||||||
+29
-35
@@ -3,7 +3,6 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
paas "proxmoxaas-common-lib"
|
|
||||||
"proxmoxaas-dashboard/app/common"
|
"proxmoxaas-dashboard/app/common"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ type InstanceStatus struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETIndex(c *gin.Context) {
|
func HandleGETIndex(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil { // user should be authed, try to return index with population
|
if err == nil { // user should be authed, try to return index with population
|
||||||
instances, _, err := GetClusterResources(auth)
|
instances, _, err := GetClusterResources(auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -65,7 +64,7 @@ func HandleGETIndex(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func HandleGETInstancesFragment(c *gin.Context) {
|
func HandleGETInstancesFragment(c *gin.Context) {
|
||||||
auth, err := common.GetAuthFromRequest(c)
|
auth, err := common.GetAuth(c)
|
||||||
if err == nil { // user should be authed, try to return index with population
|
if err == nil { // user should be authed, try to return index with population
|
||||||
instances, _, err := GetClusterResources(auth)
|
instances, _, err := GetClusterResources(auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,23 +72,25 @@ func HandleGETInstancesFragment(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.Header("Content-Type", "text/plain")
|
c.Header("Content-Type", "text/plain")
|
||||||
err = common.TMPL.ExecuteTemplate(c.Writer, "html/index-instances.go.tmpl", gin.H{
|
common.TMPL.ExecuteTemplate(c.Writer, "html/index-instances.go.tmpl", gin.H{
|
||||||
"instances": instances,
|
"instances": instances,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
c.Status(http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
c.Status(http.StatusOK)
|
c.Status(http.StatusOK)
|
||||||
}
|
|
||||||
} else { // return 401
|
} else { // return 401
|
||||||
c.Status(http.StatusUnauthorized)
|
c.Status(http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node, error) {
|
func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) {
|
||||||
body := []any{}
|
ctx := common.RequestContext{
|
||||||
res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", &auth, &body)
|
Cookies: map[string]string{
|
||||||
|
"PVEAuthCookie": auth.Token,
|
||||||
|
"CSRFPreventionToken": auth.CSRF,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -101,17 +102,16 @@ func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node
|
|||||||
nodes := map[string]Node{}
|
nodes := map[string]Node{}
|
||||||
|
|
||||||
// parse /proxmox/cluster/resources to separate instances and nodes
|
// parse /proxmox/cluster/resources to separate instances and nodes
|
||||||
for _, v := range body {
|
for _, v := range body["data"].([]any) {
|
||||||
m := v.(map[string]any)
|
m := v.(map[string]any)
|
||||||
switch m["type"] {
|
if m["type"] == "node" { // if type is node -> parse as Node object
|
||||||
case "node": // if type is node -> parse as Node object
|
|
||||||
node := Node{}
|
node := Node{}
|
||||||
err := mapstructure.Decode(v, &node)
|
err := mapstructure.Decode(v, &node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
nodes[node.Node] = node
|
nodes[node.Node] = node
|
||||||
case "lxc", "qemu": // if type is lxc or qemu -> parse as InstanceCard object
|
} else if m["type"] == "lxc" || m["type"] == "qemu" { // if type is lxc or qemu -> parse as InstanceCard object
|
||||||
instance := InstanceCard{}
|
instance := InstanceCard{}
|
||||||
err := mapstructure.Decode(v, &instance)
|
err := mapstructure.Decode(v, &instance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,10 +127,9 @@ func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node
|
|||||||
// set instance's config link path
|
// set instance's config link path
|
||||||
instance.ConfigPath = fmt.Sprintf("config?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
|
instance.ConfigPath = fmt.Sprintf("config?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
|
||||||
// set the instance's console link path
|
// set the instance's console link path
|
||||||
switch instance.Type {
|
if instance.Type == "qemu" {
|
||||||
case "qemu":
|
|
||||||
instance.ConsolePath = fmt.Sprintf("%s/?console=kvm&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&novnc=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
|
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)
|
||||||
case "lxc":
|
} 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.ConsolePath = fmt.Sprintf("%s/?console=lxc&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&xtermjs=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
|
||||||
}
|
}
|
||||||
// set the instance's backups link path
|
// set the instance's backups link path
|
||||||
@@ -139,8 +138,8 @@ func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node
|
|||||||
instances[vmid] = instance
|
instances[vmid] = instance
|
||||||
}
|
}
|
||||||
|
|
||||||
body = []any{}
|
body = map[string]any{}
|
||||||
res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", &auth, &body)
|
res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -149,10 +148,10 @@ func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node
|
|||||||
}
|
}
|
||||||
|
|
||||||
most_recent_task := map[uint]uint{}
|
most_recent_task := map[uint]uint{}
|
||||||
expected_states := map[uint]string{}
|
expected_state := map[uint]string{}
|
||||||
|
|
||||||
// iterate through recent user accessible tasks to find the task most recently made on an instance
|
// iterate through recent user accessible tasks to find the task most recently made on an instance
|
||||||
for _, v := range body {
|
for _, v := range body["data"].([]any) {
|
||||||
// parse task as Task object
|
// parse task as Task object
|
||||||
task := Task{}
|
task := Task{}
|
||||||
err := mapstructure.Decode(v, &task)
|
err := mapstructure.Decode(v, &task)
|
||||||
@@ -181,24 +180,23 @@ func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node
|
|||||||
} else { // recent task is a start or stop task for user instance which is running or "OK"
|
} 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
|
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
|
most_recent_task[task.VMID] = task.EndTime // update the most recent task
|
||||||
switch task.Type {
|
if task.Type == "qmstart" || task.Type == "vzstart" { // if the task was a start task, update the expected state to running
|
||||||
case "qmstart", "vzstart": // if the task was a start task, update the expected state to running
|
expected_state[task.VMID] = "running"
|
||||||
expected_states[task.VMID] = "running"
|
} else if task.Type == "qmstop" || task.Type == "vzstop" { // if the task was a stop task, update the expected state to stopped
|
||||||
case "qmstop", "vzstop": // if the task was a stop task, update the expected state to stopped
|
expected_state[task.VMID] = "stopped"
|
||||||
expected_states[task.VMID] = "stopped"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterate through the instances with recent tasks, refetch their state from a more reliable source
|
// iterate through the instances with recent tasks, refetch their state from a more reliable source
|
||||||
for vmid, expected_state := range expected_states { // for the expected states from recent tasks
|
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
|
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
|
// get /status/current which is updated faster than /cluster/resources
|
||||||
instance := instances[vmid]
|
instance := instances[vmid]
|
||||||
path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID)
|
path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID)
|
||||||
body := map[string]any{}
|
body = map[string]any{}
|
||||||
res, code, err := common.RequestGetAPI(path, &auth, &body)
|
res, code, err := common.RequestGetAPI(path, ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -206,12 +204,8 @@ func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node
|
|||||||
return nil, nil, fmt.Errorf("request to %s resulted in %+v", path, res)
|
return nil, nil, fmt.Errorf("request to %s resulted in %+v", path, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// attempt to decode task status as instance status
|
|
||||||
status := InstanceStatus{}
|
status := InstanceStatus{}
|
||||||
err = mapstructure.Decode(body, &status)
|
mapstructure.Decode(body["data"], &status)
|
||||||
if err != nil { // did not successfully decode task status, just skip
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.Status = status.Status
|
instance.Status = status.Status
|
||||||
instances[vmid] = instance
|
instances[vmid] = instance
|
||||||
|
|||||||
+6
-3
@@ -24,8 +24,11 @@ type Realm struct {
|
|||||||
func GetLoginRealms() ([]Realm, error) {
|
func GetLoginRealms() ([]Realm, error) {
|
||||||
realms := []Realm{}
|
realms := []Realm{}
|
||||||
|
|
||||||
body := []any{}
|
ctx := common.RequestContext{
|
||||||
res, code, err := common.RequestGetAPI("/proxmox/access/domains", nil, &body)
|
Cookies: nil,
|
||||||
|
}
|
||||||
|
body := map[string]any{}
|
||||||
|
res, code, err := common.RequestGetAPI("/proxmox/access/domains", ctx, &body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return realms, err
|
return realms, err
|
||||||
}
|
}
|
||||||
@@ -33,7 +36,7 @@ func GetLoginRealms() ([]Realm, error) {
|
|||||||
return realms, fmt.Errorf("request to /proxmox/access/domains resulted in %+v", res)
|
return realms, fmt.Errorf("request to /proxmox/access/domains resulted in %+v", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range body {
|
for _, v := range body["data"].([]any) {
|
||||||
v = v.(map[string]any)
|
v = v.(map[string]any)
|
||||||
realm := Realm{}
|
realm := Realm{}
|
||||||
err := mapstructure.Decode(v, &realm)
|
err := mapstructure.Decode(v, &realm)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func HandleGETSettings(c *gin.Context) {
|
func HandleGETSettings(c *gin.Context) {
|
||||||
_, err := common.GetAuthFromRequest(c)
|
_, err := common.GetAuth(c)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.HTML(http.StatusOK, "html/settings.html", gin.H{
|
c.HTML(http.StatusOK, "html/settings.html", gin.H{
|
||||||
"global": common.Global,
|
"global": common.Global,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module proxmoxaas-dashboard
|
module proxmoxaas-dashboard
|
||||||
|
|
||||||
go 1.26.5
|
go 1.26.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
|
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||||
github.com/tdewolff/minify/v2 v2.24.13
|
github.com/tdewolff/minify/v2 v2.24.12
|
||||||
proxmoxaas-common-lib v0.0.0
|
proxmoxaas-common-lib v0.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,33 +14,33 @@ replace proxmoxaas-common-lib => ./proxmoxaas-common-lib
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
github.com/bytedance/sonic v1.15.2 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.7 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
github.com/gin-contrib/sse v1.1.1 // 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.30.3 // indirect
|
github.com/go-playground/validator/v10 v10.30.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.6 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // 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.3.0 // 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/mattn/go-isatty v0.0.22 // indirect
|
github.com/mattn/go-isatty v0.0.21 // 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.4.0 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.60.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.8.13 // indirect
|
github.com/tdewolff/parse/v2 v2.8.12 // 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.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.6.1 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
|
||||||
golang.org/x/arch v0.28.0 // indirect
|
golang.org/x/arch v0.26.0 // indirect
|
||||||
golang.org/x/crypto v0.53.0 // indirect
|
golang.org/x/crypto v0.50.0 // indirect
|
||||||
golang.org/x/net v0.56.0 // indirect
|
golang.org/x/net v0.53.0 // indirect
|
||||||
golang.org/x/sys v0.46.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/text v0.38.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
"description": "Front-end for ProxmoxAAS",
|
"description": "Front-end for ProxmoxAAS",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "html-validate --config dev_config/.htmlvalidate.json web/html/*; stylelint --config dev_config/.stylelintrc.json --formatter verbose --fix web/css/*.css; DEBUG=eslint:cli-engine eslint --config dev_config/eslint.config.mjs --fix web/scripts/"
|
"lint": "html-validate --config dev_config/.htmlvalidate.json web/html/*; stylelint --config dev_config/.stylelintrc.json --formatter verbose --fix web/css/*.css; DEBUG=eslint:cli-engine eslint --config dev_config/eslint.config.mjs --fix web/scripts/",
|
||||||
|
"update-modules": "rm -rf web/modules/wfa.js web/modules/wfa.wasm; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.js -o web/modules/wfa.js; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.wasm -o web/modules/wfa.wasm"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.5",
|
"@eslint/eslintrc": "^3.3.5",
|
||||||
|
|||||||
+1
-1
Submodule proxmoxaas-common-lib updated: 05519694e1...cc53d7bdea
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-25
@@ -5,7 +5,7 @@ input, select, textarea {
|
|||||||
|
|
||||||
.input-grid {
|
.input-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5em 1em;
|
gap: 5px 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -13,12 +13,12 @@ input, select, textarea {
|
|||||||
.input-grid * {
|
.input-grid * {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding-top: 0.5em;
|
padding-top: 8px;
|
||||||
padding-bottom: 0.5em;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-grid input {
|
.input-grid input {
|
||||||
padding: 0.5em;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-grid img {
|
.input-grid img {
|
||||||
@@ -34,17 +34,17 @@ legend {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-top: 0.25em;
|
line-height: 1.5em;
|
||||||
margin-bottom: 0.25em;
|
margin-top: 0.25lh;
|
||||||
|
margin-bottom: 0.25lh;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset > *:last-child {
|
fieldset > *:last-child {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset > .input-grid {
|
fieldset > .input-grid {
|
||||||
@@ -66,7 +66,7 @@ input[type="radio"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.w3-select, select {
|
.w3-select, select {
|
||||||
padding: 0.5em;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w3-check {
|
.w3-check {
|
||||||
@@ -77,25 +77,10 @@ input[type="radio"] {
|
|||||||
:not(.input-grid) .input-grid + * {
|
:not(.input-grid) .input-grid + * {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.5em;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
margin: auto;
|
|
||||||
max-width: calc(min(100% - 16px, 80ch));
|
max-width: calc(min(100% - 16px, 80ch));
|
||||||
color: var(--main-text-color);
|
color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog #prompt {
|
|
||||||
margin-top: 1em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog button[value="confirm"] {
|
|
||||||
background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);
|
|
||||||
}
|
|
||||||
|
|
||||||
dialog button[value="cancel"] {
|
|
||||||
background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);
|
|
||||||
}
|
|
||||||
+6
-6
@@ -53,7 +53,7 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
font-size: var(--small-font-size);
|
font-size: 18px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: var(--nav-header-bg-color);
|
background-color: var(--nav-header-bg-color);
|
||||||
color: var(--nav-header-text-color);
|
color: var(--nav-header-text-color);
|
||||||
@@ -61,15 +61,15 @@ header h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
font-size: var(--small-font-size);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-size: larger;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav a, header h1, label[for="navtoggle"] {
|
nav a, header h1, label[for="navtoggle"] {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding-left: 0.5em;
|
padding-left: 8px;
|
||||||
padding-right: 0.5em;
|
padding-right: 8px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
height: 2em;
|
height: 2em;
|
||||||
@@ -80,7 +80,7 @@ label[for="navtoggle"], #navtoggle {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width >= 601px){
|
@media screen and (width >= 600px){
|
||||||
header {
|
header {
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ label[for="navtoggle"], #navtoggle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width <= 601px){
|
@media screen and (width <= 600px){
|
||||||
header {
|
header {
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-30
@@ -3,9 +3,6 @@
|
|||||||
--positive-color: #0f0;
|
--positive-color: #0f0;
|
||||||
--highlight-color: yellow;
|
--highlight-color: yellow;
|
||||||
--lightbg-text-color: black;
|
--lightbg-text-color: black;
|
||||||
--large-font-size: 32px;
|
|
||||||
--medium-font-size: 24px;
|
|
||||||
--small-font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (prefers-color-scheme: dark) {
|
@media screen and (prefers-color-scheme: dark) {
|
||||||
@@ -44,35 +41,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*, h1, h2, h3, h4, h5, h6, p {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
line-height: normal;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: var(--large-font-size);
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: var(--medium-font-size);
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h4, legend {
|
|
||||||
font-size: var(--small-font-size);
|
|
||||||
text-decoration: underline;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: var(--small-font-size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
+41
-8
@@ -34,25 +34,58 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<h2>Account</h2>
|
<h2>Account</h2>
|
||||||
<section class="w3-card w3-padding">
|
<section class="w3-card w3-padding">
|
||||||
<h3>Account Details</h3>
|
<h3>Account Details</h3>
|
||||||
<p id="username">Username: {{.account.Username.UserID}}@{{.account.Username.Realm}}</p>
|
<p id="username">Username: {{.account.Username}}</p>
|
||||||
<p id="email">Email: {{.account.Mail}}</p>
|
<p id="pool">Pools: {{MapKeys .account.Pools ", "}}</p>
|
||||||
<p>Password: <button class="w3-button" id="change-password" type="button" style="padding: 0em; height: 1.5em; line-height: 1.5em;">Change Password</button></p>
|
<p id="vmid">VMID Range: {{.account.VMID.Min}} - {{.account.VMID.Max}}</p>
|
||||||
|
<p id="nodes">Nodes: {{MapKeys .account.Nodes ", "}}</p>
|
||||||
</section>
|
</section>
|
||||||
{{range $poolname, $pool := .account.Pools}}
|
<section class="w3-card w3-padding">
|
||||||
{{template "pool-resources" $pool}}
|
<div class="flex row nowrap">
|
||||||
|
<h3>Password</h3>
|
||||||
|
<button class="w3-button w3-margin" id="change-password" type="button">Change Password</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="w3-card w3-padding">
|
||||||
|
<h3>Cluster Resources</h3>
|
||||||
|
<div>
|
||||||
|
{{range $category, $v := .account.Resources}}
|
||||||
|
{{if ne $category ""}}
|
||||||
|
<h4>{{$category}}</h4>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
<div class="resource-container">
|
||||||
|
{{range $v}}
|
||||||
|
{{if .Display}}
|
||||||
|
{{if eq .Type "numeric"}}
|
||||||
|
{{template "resource-chart" .}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Type "storage"}}
|
||||||
|
{{template "resource-chart" .}}
|
||||||
|
{{end}}
|
||||||
|
{{if eq .Type "list"}}
|
||||||
|
{{range .Resources}}
|
||||||
|
{{template "resource-chart" .}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<template id="change-password-dialog">
|
<template id="change-password-dialog">
|
||||||
<link rel="stylesheet" href="modules/w3.css">
|
<link rel="stylesheet" href="modules/w3.css">
|
||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Change Password
|
Change Password
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -64,8 +97,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<h2><a href="index">Instances</a> / {{.config.Name}} / Backups</h2>
|
<h2><a href="index">Instances</a> / {{.config.Name}} / Backups</h2>
|
||||||
<section class="w3-card w3-padding">
|
<section class="w3-card w3-padding">
|
||||||
|
|||||||
@@ -19,10 +19,12 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<h2><a href="index">Instances</a>/{{.config.Name}}/Config</h2>
|
<h2><a href="index">Instances</a> / {{.config.Name}} / Config</h2>
|
||||||
<form id="config-form">
|
<form id="config-form">
|
||||||
<fieldset class="w3-card w3-padding">
|
<fieldset class="w3-card w3-padding">
|
||||||
<legend>Resources</legend>
|
<legend>Resources</legend>
|
||||||
|
|||||||
+8
-6
@@ -65,14 +65,16 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section>
|
||||||
<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" tabindex="0">
|
<form id="vm-search" role="search" class="flex row nowrap" tabindex="0">
|
||||||
<button type="submit" id="submit" class="w3-button" style="padding: 0; width: 1em; height: 1em; line-height: 1em;"><img alt="Search Instances" aria-label="Search Instances" src="images/common/search.svg#symb"></button>
|
<img alt="Search Instances" aria-label="Search Instances" src="images/common/search.svg#symb">
|
||||||
<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-->
|
<!--Add Instance Button & Dialog Template-->
|
||||||
@@ -82,7 +84,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<template id="create-instance-dialog">
|
<template id="create-instance-dialog">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Create New Instance
|
Create New Instance
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -92,14 +94,14 @@
|
|||||||
<option value="lxc">Container</option>
|
<option value="lxc">Container</option>
|
||||||
<option value="qemu">Virtual Machine</option>
|
<option value="qemu">Virtual Machine</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="pool">Pool</label>
|
|
||||||
<select class="w3-select w3-border" name="pool" id="pool" required></select>
|
|
||||||
<label for="node">Node</label>
|
<label for="node">Node</label>
|
||||||
<select class="w3-select w3-border" name="node" id="node" required></select>
|
<select class="w3-select w3-border" name="node" id="node" required></select>
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input class="w3-input w3-border" name="name" id="name" type="text" required>
|
<input class="w3-input w3-border" name="name" id="name" type="text" required>
|
||||||
<label for="vmid">ID</label>
|
<label for="vmid">ID</label>
|
||||||
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required>
|
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required>
|
||||||
|
<label for="pool">Pool</label>
|
||||||
|
<select class="w3-select w3-border" name="pool" id="pool" required></select>
|
||||||
<label for="cores">Cores (Threads)</label>
|
<label for="cores">Cores (Threads)</label>
|
||||||
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required>
|
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required>
|
||||||
<label for="memory">Memory (MiB)</label>
|
<label for="memory">Memory (MiB)</label>
|
||||||
@@ -120,8 +122,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
<link rel="modulepreload" href="scripts/dialog.js">
|
<link rel="modulepreload" href="scripts/dialog.js">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
</header>
|
||||||
<main class="flex" style="justify-content: center; align-items: center;">
|
<main class="flex" style="justify-content: center; align-items: center;">
|
||||||
<div class="w3-container w3-card w3-margin w3-padding" style="height: fit-content;">
|
<div class="w3-container w3-card w3-margin w3-padding" style="height: fit-content;">
|
||||||
<h2 class="w3-center">{{.global.Organization}} Login</h2>
|
<h2 class="w3-center">{{.global.Organization}} Login</h2>
|
||||||
|
|||||||
+10
-13
@@ -5,31 +5,30 @@
|
|||||||
<script src="scripts/settings.js" type="module"></script>
|
<script src="scripts/settings.js" type="module"></script>
|
||||||
<link rel="modulepreload" href="scripts/utils.js">
|
<link rel="modulepreload" href="scripts/utils.js">
|
||||||
<style>
|
<style>
|
||||||
|
legend {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
label {
|
label {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
column-gap: 0.5em;
|
column-gap: 10px;
|
||||||
}
|
}
|
||||||
label + p {
|
label + p {
|
||||||
margin-top: 0.5em;
|
margin-top: 5px;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 25px;
|
||||||
}
|
}
|
||||||
p:last-child {
|
p:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0px;
|
||||||
}
|
|
||||||
#save.disabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#save.enabled {
|
|
||||||
display: unset;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
<form id="settings">
|
<form id="settings">
|
||||||
@@ -43,8 +42,6 @@
|
|||||||
<p>App will periodically check for updates and synchronize only if needed. Medium resource usage.</p>
|
<p>App will periodically check for updates and synchronize only if needed. Medium resource usage.</p>
|
||||||
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
|
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
|
||||||
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
|
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
|
||||||
<label><input class="w3-radio" type="radio" id="sync-never" name="sync-scheme" value="never" required>Never Sync</label>
|
|
||||||
<p>App will never automatically sync. Reload the page to sync the latest cluster state.</p>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>App Sync Frequency</legend>
|
<legend>App Sync Frequency</legend>
|
||||||
@@ -76,7 +73,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</section>
|
</section>
|
||||||
<div class="w3-container w3-center" id="form-actions">
|
<div class="w3-container w3-center" id="form-actions">
|
||||||
<button class="w3-button w3-margin disabled" id="save" type="submit">SAVE</button>
|
<button class="w3-button w3-margin" id="save" type="submit">SAVE</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../../common/config-inactive.svg
|
|
||||||
|
Before Width: | Height: | Size: 32 B After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg id="symb" role="img" aria-label="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="#808080"/></svg>
|
||||||
|
Before Width: | Height: | Size: 32 B After Width: | Height: | Size: 1.0 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg id="symb" role="img" aria-label="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="#808080"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB |
+2
-579
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -83,7 +83,7 @@ class BackupCard extends HTMLElement {
|
|||||||
|
|
||||||
async handleDeleteButton () {
|
async handleDeleteButton () {
|
||||||
const template = this.shadowRoot.querySelector("#delete-dialog");
|
const template = this.shadowRoot.querySelector("#delete-dialog");
|
||||||
dialog(template, async (result, _form) => {
|
dialog(template, async (result, form) => {
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const body = {
|
const body = {
|
||||||
volid: this.volid
|
volid: this.volid
|
||||||
@@ -99,7 +99,7 @@ class BackupCard extends HTMLElement {
|
|||||||
|
|
||||||
async handleRestoreButton () {
|
async handleRestoreButton () {
|
||||||
const template = this.shadowRoot.querySelector("#restore-dialog");
|
const template = this.shadowRoot.querySelector("#restore-dialog");
|
||||||
dialog(template, async (result, _form) => {
|
dialog(template, async (result, form) => {
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
const body = {
|
const body = {
|
||||||
volid: this.volid
|
volid: this.volid
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { getSetting, requestAPI } from "./utils.js";
|
import { getSyncSettings, requestAPI } from "./utils.js";
|
||||||
|
|
||||||
export async function setupClientSync (callback) {
|
export async function setupClientSync (callback) {
|
||||||
const scheme = getSetting("sync-scheme");
|
const { scheme, rate } = getSyncSettings();
|
||||||
const rate = getSetting("sync-rate");
|
|
||||||
if (scheme === "never") {
|
if (scheme === "always") {
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (scheme === "always") {
|
|
||||||
window.setInterval(callback, rate * 1000);
|
window.setInterval(callback, rate * 1000);
|
||||||
}
|
}
|
||||||
else if (scheme === "hash") {
|
else if (scheme === "hash") {
|
||||||
@@ -22,7 +19,7 @@ export async function setupClientSync (callback) {
|
|||||||
}
|
}
|
||||||
else if (scheme === "interrupt") {
|
else if (scheme === "interrupt") {
|
||||||
const socket = new WebSocket(`wss://${window.API.replace("https://", "")}/sync/interrupt`);
|
const socket = new WebSocket(`wss://${window.API.replace("https://", "")}/sync/interrupt`);
|
||||||
socket.addEventListener("open", (_event) => {
|
socket.addEventListener("open", (event) => {
|
||||||
socket.send(`rate ${rate}`);
|
socket.send(`rate ${rate}`);
|
||||||
});
|
});
|
||||||
socket.addEventListener("message", (event) => {
|
socket.addEventListener("message", (event) => {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class VolumeAction extends HTMLElement {
|
|||||||
|
|
||||||
async handleDiskDetach () {
|
async handleDiskDetach () {
|
||||||
const disk = this.dataset.volume;
|
const disk = this.dataset.volume;
|
||||||
dialog(this.template, async (result, _form) => {
|
dialog(this.template, 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");
|
||||||
@@ -136,7 +136,7 @@ class VolumeAction extends HTMLElement {
|
|||||||
|
|
||||||
async handleDiskDelete () {
|
async handleDiskDelete () {
|
||||||
const disk = this.dataset.volume;
|
const disk = this.dataset.volume;
|
||||||
dialog(this.template, async (result, _form) => {
|
dialog(this.template, 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");
|
||||||
@@ -224,7 +224,7 @@ async function handleCDAdd () {
|
|||||||
const isos = await requestAPI("/user/vm-isos", "GET");
|
const isos = await requestAPI("/user/vm-isos", "GET");
|
||||||
const select = d.querySelector("#iso-select");
|
const select = d.querySelector("#iso-select");
|
||||||
|
|
||||||
for (const iso of isos.data) {
|
for (const iso of isos) {
|
||||||
select.add(new Option(iso.name, iso.volid));
|
select.add(new Option(iso.name, iso.volid));
|
||||||
}
|
}
|
||||||
select.selectedIndex = -1;
|
select.selectedIndex = -1;
|
||||||
@@ -275,7 +275,7 @@ class NetworkAction extends HTMLElement {
|
|||||||
|
|
||||||
async handleNetworkDelete () {
|
async handleNetworkDelete () {
|
||||||
const netID = this.dataset.network;
|
const netID = this.dataset.network;
|
||||||
dialog(this.template, async (result, _form) => {
|
dialog(this.template, async (result, form) => {
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
setIconSrc(document.querySelector(`svg[data-network="${netID}"]`), "images/status/loading.svg");
|
setIconSrc(document.querySelector(`svg[data-network="${netID}"]`), "images/status/loading.svg");
|
||||||
const net = `${netID}`;
|
const net = `${netID}`;
|
||||||
@@ -375,7 +375,7 @@ class DeviceAction extends HTMLElement {
|
|||||||
|
|
||||||
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
|
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
|
||||||
d.querySelector("#device").append(new Option(deviceName, deviceDetails.split(",")[0]));
|
d.querySelector("#device").append(new Option(deviceName, deviceDetails.split(",")[0]));
|
||||||
for (const availDevice of availDevices.data) {
|
for (const availDevice of availDevices) {
|
||||||
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.device_bus));
|
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.device_bus));
|
||||||
}
|
}
|
||||||
d.querySelector("#pcie").checked = deviceDetails.includes("pcie=1");
|
d.querySelector("#pcie").checked = deviceDetails.includes("pcie=1");
|
||||||
@@ -383,7 +383,7 @@ class DeviceAction extends HTMLElement {
|
|||||||
|
|
||||||
async handleDeviceDelete () {
|
async handleDeviceDelete () {
|
||||||
const deviceID = this.dataset.device;
|
const deviceID = this.dataset.device;
|
||||||
dialog(this.template, async (result, _form) => {
|
dialog(this.template, async (result, form) => {
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.setStatusLoading();
|
this.setStatusLoading();
|
||||||
const device = `${deviceID}`;
|
const device = `${deviceID}`;
|
||||||
@@ -437,8 +437,8 @@ async function handleDeviceAdd () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const availDevices = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci`, "GET");
|
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
|
||||||
for (const availDevice of availDevices.data) {
|
for (const availDevice of availDevices) {
|
||||||
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.device_bus));
|
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.device_bus));
|
||||||
}
|
}
|
||||||
d.querySelector("#pcie").checked = true;
|
d.querySelector("#pcie").checked = true;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* body contains an optional form or other information,
|
* body contains an optional form or other information,
|
||||||
* and controls contains a series of buttons which controls the form
|
* and controls contains a series of buttons which controls the form
|
||||||
*/
|
*/
|
||||||
export function dialog (template, onclose = async (_result, _form) => { }) {
|
export function dialog (template, onclose = async (result, form) => { }) {
|
||||||
const dialog = template.content.querySelector("dialog").cloneNode(true);
|
const dialog = template.content.querySelector("dialog").cloneNode(true);
|
||||||
document.body.append(dialog);
|
document.body.append(dialog);
|
||||||
dialog.addEventListener("close", async () => {
|
dialog.addEventListener("close", async () => {
|
||||||
@@ -48,8 +48,7 @@ export function alert (message) {
|
|||||||
dialog.id = "alert-dialog";
|
dialog.id = "alert-dialog";
|
||||||
dialog.innerHTML = `
|
dialog.innerHTML = `
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<p class="w3-large" id="prompt">Alert</p>
|
<p class="w3-center" style="margin-bottom: 0px;">${message}</p>
|
||||||
<p class="w3-center" style="margin-bottom: 0;">${message}</p>
|
|
||||||
<div class="w3-center">
|
<div class="w3-center">
|
||||||
<button class="w3-button w3-margin" id="submit">OK</button>
|
<button class="w3-button w3-margin" id="submit">OK</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,18 +82,18 @@ class ErrorDialog extends HTMLElement {
|
|||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<style>
|
<style>
|
||||||
#errors {
|
#errors {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0px;
|
||||||
max-height: 20lh;
|
max-height: 20lh;
|
||||||
min-height: 20lh;
|
min-height: 20lh;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
#errors * {
|
#errors * {
|
||||||
margin: 0;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<p class="w3-large" id="prompt">Error</p>
|
<p class="w3-large" id="prompt" style="text-align: center;">Error</p>
|
||||||
<div id="errors" class="flex column-reverse"></div>
|
<div id="errors" class="flex column-reverse"></div>
|
||||||
<div class="w3-center" id="controls">
|
<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="ok">OK</button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class DraggableContainer extends HTMLElement {
|
|||||||
window.Sortable.create(this.content, {
|
window.Sortable.create(this.content, {
|
||||||
group: this.dataset.group,
|
group: this.dataset.group,
|
||||||
ghostClass: "ghost",
|
ghostClass: "ghost",
|
||||||
setData: function (dataTransfer, _dragEl) {
|
setData: function (dataTransfer, dragEl) {
|
||||||
dataTransfer.setDragImage(blank, 0, 0);
|
dataTransfer.setDragImage(blank, 0, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+47
-43
@@ -1,4 +1,4 @@
|
|||||||
import { requestPVE, requestAPI, setAppearance, getSetting, requestDash, setIconSrc, setIconAlt } from "./utils.js";
|
import { requestPVE, requestAPI, setAppearance, getSearchSettings, requestDash, setIconSrc, setIconAlt } from "./utils.js";
|
||||||
import { alert, dialog, error } from "./dialog.js";
|
import { alert, dialog, error } 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";
|
||||||
@@ -159,7 +159,7 @@ class InstanceCard extends HTMLElement {
|
|||||||
async handlePowerButton () {
|
async handlePowerButton () {
|
||||||
if (!this.actionLock) {
|
if (!this.actionLock) {
|
||||||
const template = this.shadowRoot.querySelector("#power-dialog");
|
const template = this.shadowRoot.querySelector("#power-dialog");
|
||||||
dialog(template, async (result, _form) => {
|
dialog(template, 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";
|
||||||
@@ -193,7 +193,7 @@ class InstanceCard extends HTMLElement {
|
|||||||
handleDeleteButton () {
|
handleDeleteButton () {
|
||||||
if (!this.actionLock && this.status === "stopped") {
|
if (!this.actionLock && this.status === "stopped") {
|
||||||
const template = this.shadowRoot.querySelector("#delete-dialog");
|
const template = this.shadowRoot.querySelector("#delete-dialog");
|
||||||
dialog(template, async (result, _form) => {
|
dialog(template, async (result, form) => {
|
||||||
if (result === "confirm") {
|
if (result === "confirm") {
|
||||||
this.actionLock = true;
|
this.actionLock = true;
|
||||||
|
|
||||||
@@ -243,11 +243,11 @@ function initInstances () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sortInstances () {
|
function sortInstances () {
|
||||||
const searchCriteria = getSetting("search-criteria");
|
const searchCriteria = getSearchSettings();
|
||||||
const searchQuery = document.querySelector("#search").value || null;
|
const searchQuery = document.querySelector("#search").value || null;
|
||||||
let criteria;
|
let criteria;
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
criteria = (item, _query = null) => {
|
criteria = (item, query = null) => {
|
||||||
return { score: item.vmid, alignment: null };
|
return { score: item.vmid, alignment: null };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -343,10 +343,10 @@ async function handleInstanceAddButton () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// setup type select
|
const templates = await requestAPI("/user/ct-templates", "GET");
|
||||||
|
|
||||||
const typeSelect = d.querySelector("#type");
|
const typeSelect = d.querySelector("#type");
|
||||||
typeSelect.selectedIndex = -1;
|
typeSelect.selectedIndex = -1;
|
||||||
// on type change, reveal or hide the container specific section
|
|
||||||
typeSelect.addEventListener("change", () => {
|
typeSelect.addEventListener("change", () => {
|
||||||
if (typeSelect.value === "qemu") {
|
if (typeSelect.value === "qemu") {
|
||||||
d.querySelectorAll(".container-specific").forEach((element) => {
|
d.querySelectorAll(".container-specific").forEach((element) => {
|
||||||
@@ -366,62 +366,66 @@ async function handleInstanceAddButton () {
|
|||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// setup pool select
|
const rootfsContent = "rootdir";
|
||||||
const poolSelect = d.querySelector("#pool");
|
const rootfsStorage = d.querySelector("#rootfs-storage");
|
||||||
poolSelect.innerHTML = "";
|
rootfsStorage.selectedIndex = -1;
|
||||||
// add user pools to selector
|
|
||||||
const userPools = Object.keys((await requestAPI("/access/pools", "GET")).data.pools);
|
const userResources = await requestAPI("/user/dynamic/resources", "GET");
|
||||||
userPools.forEach((element) => {
|
const userCluster = await requestAPI("/user/config/cluster", "GET");
|
||||||
poolSelect.add(new Option(element));
|
|
||||||
});
|
|
||||||
poolSelect.selectedIndex = -1;
|
|
||||||
// on pool change, get the allowed nodes for that pool, then repopulate the node selector
|
|
||||||
poolSelect.addEventListener("change", async () => {
|
|
||||||
const pool = (await requestAPI(`/access/pools/${poolSelect.value}`, "GET")).data.pool;
|
|
||||||
|
|
||||||
const nodeSelect = d.querySelector("#node");
|
const nodeSelect = d.querySelector("#node");
|
||||||
nodeSelect.innerHTML = "";
|
nodeSelect.innerHTML = "";
|
||||||
const clusterNodes = (await requestPVE("/nodes", "GET")).data;
|
const clusterNodes = await requestPVE("/nodes", "GET");
|
||||||
const allowedNodes = Object.keys(pool["nodes-allowed"]);
|
const allowedNodes = Object.keys(userCluster.nodes);
|
||||||
clusterNodes.forEach((element) => {
|
clusterNodes.data.forEach((element) => {
|
||||||
if (element.status === "online" && allowedNodes.includes(element.node)) {
|
if (element.status === "online" && allowedNodes.includes(element.node)) {
|
||||||
nodeSelect.add(new Option(element.node));
|
nodeSelect.add(new Option(element.node));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
nodeSelect.selectedIndex = -1;
|
nodeSelect.selectedIndex = -1;
|
||||||
|
|
||||||
// set vmid min/max
|
|
||||||
d.querySelector("#vmid").min = pool["vmid-allowed"].min;
|
|
||||||
d.querySelector("#vmid").max = pool["vmid-allowed"].max;
|
|
||||||
});
|
|
||||||
|
|
||||||
// setup node select
|
|
||||||
const nodeSelect = d.querySelector("#node");
|
|
||||||
nodeSelect.selectedIndex = -1;
|
|
||||||
// on node change, get the available storages and repopulate the storage selector
|
|
||||||
nodeSelect.addEventListener("change", async () => { // change rootfs storage based on node
|
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")).data;
|
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
|
||||||
rootfsStorage.innerHTML = "";
|
rootfsStorage.innerHTML = "";
|
||||||
storage.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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
rootfsStorage.selectedIndex = -1;
|
rootfsStorage.selectedIndex = -1;
|
||||||
|
|
||||||
|
// set core and memory min/max depending on node selected
|
||||||
|
if (node in userResources.cores.nodes) {
|
||||||
|
d.querySelector("#cores").max = userResources.cores.nodes[node].avail;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
d.querySelector("#cores").max = userResources.cores.global.avail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node in userResources.memory.nodes) {
|
||||||
|
d.querySelector("#memory").max = userResources.memory.nodes[node].avail;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
d.querySelector("#memory").max = userResources.memory.global.avail;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// setup root dir select
|
// set vmid min/max
|
||||||
const rootfsStorage = d.querySelector("#rootfs-storage");
|
d.querySelector("#vmid").min = userCluster.vmid.min;
|
||||||
rootfsStorage.selectedIndex = -1;
|
d.querySelector("#vmid").max = userCluster.vmid.max;
|
||||||
// set rootfs content type (rootdir)
|
|
||||||
const rootfsContent = "rootdir";
|
// add user pools to selector
|
||||||
|
const poolSelect = d.querySelector("#pool");
|
||||||
|
poolSelect.innerHTML = "";
|
||||||
|
const userPools = Object.keys(userCluster.pools);
|
||||||
|
userPools.forEach((element) => {
|
||||||
|
poolSelect.add(new Option(element));
|
||||||
|
});
|
||||||
|
poolSelect.selectedIndex = -1;
|
||||||
|
|
||||||
// setup templateImage depending on selected image storage
|
|
||||||
const templateImage = d.querySelector("#template-image");
|
|
||||||
// add template images to selector
|
// add template images to selector
|
||||||
const templates = await requestAPI("/user/ct-templates", "GET");
|
const templateImage = d.querySelector("#template-image"); // populate templateImage depending on selected image storage
|
||||||
for (const template of templates.data) {
|
for (const template of templates) {
|
||||||
templateImage.append(new Option(template.name, template.volid));
|
templateImage.append(new Option(template.name, template.volid));
|
||||||
}
|
}
|
||||||
templateImage.selectedIndex = -1;
|
templateImage.selectedIndex = -1;
|
||||||
|
|||||||
+19
-46
@@ -1,62 +1,35 @@
|
|||||||
import { setAppearance, getSetting, setSetting } from "./utils.js";
|
import { setAppearance, getSyncSettings, getSearchSettings, getThemeSettings, setSyncSettings, setSearchSettings, setThemeSettings } from "./utils.js";
|
||||||
|
|
||||||
window.addEventListener("DOMContentLoaded", init);
|
window.addEventListener("DOMContentLoaded", init);
|
||||||
|
|
||||||
function init () {
|
function init () {
|
||||||
setAppearance();
|
setAppearance();
|
||||||
|
const { scheme, rate } = getSyncSettings();
|
||||||
|
if (scheme) {
|
||||||
|
document.querySelector(`#sync-${scheme}`).checked = true;
|
||||||
|
}
|
||||||
|
if (rate) {
|
||||||
|
document.querySelector("#sync-rate").value = rate;
|
||||||
|
}
|
||||||
|
|
||||||
document.querySelectorAll("[id^=sync-]").forEach((v) => {
|
const search = getSearchSettings();
|
||||||
v.addEventListener("change", handleSettingsChange);
|
if (search) {
|
||||||
});
|
document.querySelector(`#search-${search}`).checked = true;
|
||||||
document.querySelector(`#sync-${getSetting("sync-scheme")}`).checked = true;
|
}
|
||||||
document.querySelector("#sync-rate").value = getSetting("sync-rate");
|
|
||||||
|
|
||||||
document.querySelectorAll("[id^=search-]").forEach((v) => {
|
const theme = getThemeSettings();
|
||||||
v.addEventListener("change", handleSettingsChange);
|
if (theme) {
|
||||||
});
|
document.querySelector("#appearance-theme").value = theme;
|
||||||
document.querySelector(`#search-${getSetting("search-criteria")}`).checked = true;
|
}
|
||||||
|
|
||||||
document.querySelector("#appearance-theme").addEventListener("change", handleSettingsChange);
|
|
||||||
document.querySelector("#appearance-theme").value = getSetting("appearance-theme");
|
|
||||||
|
|
||||||
document.querySelector("#settings").addEventListener("submit", handleSaveSettings, false);
|
document.querySelector("#settings").addEventListener("submit", handleSaveSettings, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSettingsChange (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const form = new FormData(document.querySelector("#settings"));
|
|
||||||
const saveBtn = document.querySelector("#save");
|
|
||||||
if (getSetting("sync-scheme") !== form.get("sync-scheme")) {
|
|
||||||
saveBtn.classList.remove("disabled");
|
|
||||||
saveBtn.classList.add("enabled");
|
|
||||||
}
|
|
||||||
else if (getSetting("sync-rate") !== Number(form.get("sync-rate"))) {
|
|
||||||
saveBtn.classList.remove("disabled");
|
|
||||||
saveBtn.classList.add("enabled");
|
|
||||||
}
|
|
||||||
else if (getSetting("search-criteria") !== form.get("search-criteria")) {
|
|
||||||
saveBtn.classList.remove("disabled");
|
|
||||||
saveBtn.classList.add("enabled");
|
|
||||||
}
|
|
||||||
else if (getSetting("appearance-theme") !== form.get("appearance-theme")) {
|
|
||||||
saveBtn.classList.remove("disabled");
|
|
||||||
saveBtn.classList.add("enabled");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
saveBtn.classList.remove("enabled");
|
|
||||||
saveBtn.classList.add("disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSaveSettings (event) {
|
function handleSaveSettings (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const form = new FormData(document.querySelector("#settings"));
|
const form = new FormData(document.querySelector("#settings"));
|
||||||
const saveBtn = document.querySelector("#save");
|
setSyncSettings(form.get("sync-scheme"), form.get("sync-rate"));
|
||||||
setSetting("sync-scheme", form.get("sync-scheme"));
|
setSearchSettings(form.get("search-criteria"));
|
||||||
setSetting("sync-rate", Number(form.get("sync-rate")));
|
setThemeSettings(form.get("appearance-theme"));
|
||||||
setSetting("search-criteria", form.get("search-criteria"));
|
|
||||||
setSetting("appearance-theme", form.get("appearance-theme"));
|
|
||||||
saveBtn.classList.remove("enabled");
|
|
||||||
saveBtn.classList.add("disabled");
|
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-29
@@ -80,34 +80,33 @@ async function request (url, content) {
|
|||||||
try {
|
try {
|
||||||
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");
|
||||||
const res = {};
|
let data = null;
|
||||||
|
|
||||||
if (contentType === null) {
|
if (contentType === null) {
|
||||||
res.data = null;
|
data = {};
|
||||||
res.status = response.status;
|
|
||||||
}
|
}
|
||||||
else if (contentType.includes("application/json")) {
|
else if (contentType.includes("application/json")) {
|
||||||
res.data = await response.json();
|
data = await response.json();
|
||||||
res.status = response.status;
|
data.status = response.status;
|
||||||
}
|
}
|
||||||
else if (contentType.includes("text/html")) {
|
else if (contentType.includes("text/html")) {
|
||||||
res.data = await response.text();
|
data = { data: await response.text() };
|
||||||
res.status = response.status;
|
data.status = response.status;
|
||||||
}
|
}
|
||||||
else if (contentType.includes("text/plain")) {
|
else if (contentType.includes("text/plain")) {
|
||||||
res.data = await response.text();
|
data = { data: await response.text() };
|
||||||
res.status = response.status;
|
data.status = response.status;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.data = null;
|
data = {};
|
||||||
res.status = response.status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return { status: response.status, error: res.data ? res.data.error : response.status };
|
return { status: response.status, error: data ? data.error : response.status };
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return res;
|
data.status = response.status;
|
||||||
|
return data || response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
@@ -125,30 +124,60 @@ export function getURIData () {
|
|||||||
return Object.fromEntries(url.searchParams);
|
return Object.fromEntries(url.searchParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = {
|
const settingsDefault = {
|
||||||
"sync-scheme": {"type": String, "default": "always"},
|
"sync-scheme": "always",
|
||||||
"sync-rate": {"type": Number, "default": 5},
|
"sync-rate": 5,
|
||||||
"search-criteria": {"type": String, "default": "fuzzy"},
|
"search-criteria": "fuzzy",
|
||||||
"appearance-theme": {"type": String, "default": "auto"}
|
"appearance-theme": "auto"
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getSetting (key) {
|
export function getSyncSettings () {
|
||||||
const meta = settings[key];
|
let scheme = localStorage.getItem("sync-scheme");
|
||||||
let value = localStorage.getItem(key);
|
let rate = Number(localStorage.getItem("sync-rate"));
|
||||||
if (value === null || meta === null) {
|
if (!scheme) {
|
||||||
value = meta.default;
|
scheme = settingsDefault["sync-scheme"];
|
||||||
localStorage.setItem(key, meta.default);
|
localStorage.setItem("sync-scheme", scheme);
|
||||||
}
|
}
|
||||||
|
if (!rate) {
|
||||||
return meta.type(value);
|
rate = settingsDefault["sync-rate"];
|
||||||
|
localStorage.setItem("sync-rate", rate);
|
||||||
|
}
|
||||||
|
return { scheme, rate };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSetting (key, value) {
|
export function getSearchSettings () {
|
||||||
localStorage.setItem(key, value);
|
let searchCriteria = localStorage.getItem("search-criteria");
|
||||||
|
if (!searchCriteria) {
|
||||||
|
searchCriteria = settingsDefault["search-criteria"];
|
||||||
|
localStorage.setItem("search-criteria", searchCriteria);
|
||||||
|
}
|
||||||
|
return searchCriteria;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThemeSettings () {
|
||||||
|
let theme = localStorage.getItem("appearance-theme");
|
||||||
|
if (!theme) {
|
||||||
|
theme = settingsDefault["appearance-theme"];
|
||||||
|
localStorage.setItem("appearance-theme", theme);
|
||||||
|
}
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSyncSettings (scheme, rate) {
|
||||||
|
localStorage.setItem("sync-scheme", scheme);
|
||||||
|
localStorage.setItem("sync-rate", rate);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSearchSettings (criteria) {
|
||||||
|
localStorage.setItem("search-criteria", criteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setThemeSettings (theme) {
|
||||||
|
localStorage.setItem("appearance-theme", theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setAppearance () {
|
export function setAppearance () {
|
||||||
const theme = getSetting("appearance-theme");
|
const theme = getThemeSettings();
|
||||||
if (theme === "auto") {
|
if (theme === "auto") {
|
||||||
document.querySelector(":root").classList.remove("dark-theme", "light-theme");
|
document.querySelector(":root").classList.remove("dark-theme", "light-theme");
|
||||||
}
|
}
|
||||||
@@ -162,6 +191,7 @@ export function setAppearance () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// assumes href is path to svg, and id to grab is #symb
|
||||||
export function setIconSrc (icon, path) {
|
export function setIconSrc (icon, path) {
|
||||||
icon.setAttribute("src", path);
|
icon.setAttribute("src", path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
a {
|
a {
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
margin: 0;
|
margin: 0px;
|
||||||
padding: 0;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="w3-row" style="margin-top: 1em; margin-bottom: 1em;">
|
<div class="w3-row" style="margin-top: 1em; margin-bottom: 1em;">
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Edit Backup
|
Edit Backup
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -39,8 +39,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Delete Backup
|
Delete Backup
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -60,8 +60,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Restore From Backup?
|
Restore From Backup?
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -84,8 +84,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Create Backup
|
Create Backup
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -113,8 +113,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{{/* <head> common across all pages*/}}
|
|
||||||
{{define "head"}}
|
{{define "head"}}
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -15,13 +14,11 @@
|
|||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{/* <header> common across all pages*/}}
|
|
||||||
{{define "header"}}
|
{{define "header"}}
|
||||||
<header>
|
<h1>{{.global.Organization}}</h1>
|
||||||
<h1>{{.global.Organization}}</h1>
|
<label for="navtoggle">☰</label>
|
||||||
<label for="navtoggle">☰</label>
|
<input type="checkbox" id="navtoggle">
|
||||||
<input type="checkbox" id="navtoggle">
|
<nav id="navigation">
|
||||||
<nav id="navigation">
|
|
||||||
{{if eq .page "login"}}
|
{{if eq .page "login"}}
|
||||||
<a href="login" aria-current="page">Login</a>
|
<a href="login" aria-current="page">Login</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
@@ -30,6 +27,5 @@
|
|||||||
<a href="settings" {{if eq .page "settings"}} aria-current="page" {{end}}>Settings</a>
|
<a href="settings" {{if eq .page "settings"}} aria-current="page" {{end}}>Settings</a>
|
||||||
<a href="login">Logout</a>
|
<a href="login">Logout</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<template id="add-disk-dialog">
|
<template id="add-disk-dialog">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Create New Disk
|
Create New Disk
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -65,8 +65,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<template id="add-cd-dialog">
|
<template id="add-cd-dialog">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Mount a CDROM
|
Mount a CDROM
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -89,8 +89,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -163,7 +163,7 @@
|
|||||||
<img class="clickable" alt="Move {{.Name}}" src="images/actions/disk/move-active.svg#symb">
|
<img class="clickable" alt="Move {{.Name}}" src="images/actions/disk/move-active.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Move {{.Name}}
|
Move {{.Name}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -173,8 +173,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
<img class="clickable" alt="Resize {{.Name}}" src="images/actions/disk/resize-active.svg#symb">
|
<img class="clickable" alt="Resize {{.Name}}" src="images/actions/disk/resize-active.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Resize {{.Name}}
|
Resize {{.Name}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -208,8 +208,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
<img class="clickable" alt="Delete {{.Name}}" src="images/actions/disk/delete-active.svg#symb">
|
<img class="clickable" alt="Delete {{.Name}}" src="images/actions/disk/delete-active.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Delete {{.Name}}
|
Delete {{.Name}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -242,8 +242,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -267,7 +267,7 @@
|
|||||||
<img class="clickable" alt="Attach {{.Name}}" src="images/actions/disk/attach.svg#symb">
|
<img class="clickable" alt="Attach {{.Name}}" src="images/actions/disk/attach.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Attach {{.Name}}
|
Attach {{.Name}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -284,8 +284,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
<img class="clickable" alt="Detach {{.Name}}" src="images/actions/disk/detach.svg#symb">
|
<img class="clickable" alt="Detach {{.Name}}" src="images/actions/disk/detach.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Detach {{.Name}}
|
Detach {{.Name}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -309,8 +309,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<template id="add-net-dialog">
|
<template id="add-net-dialog">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Create Network Interface
|
Create Network Interface
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -353,8 +353,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -371,7 +371,7 @@
|
|||||||
<img class="clickable" alt="Configure Net {{.Net_ID}}" src="images/actions/network/config.svg#symb">
|
<img class="clickable" alt="Configure Net {{.Net_ID}}" src="images/actions/network/config.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Edit {{.Net_ID}}
|
Edit {{.Net_ID}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -380,8 +380,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -393,7 +393,7 @@
|
|||||||
<img class="clickable" alt="Delete Net {{.Net_ID}}" src="images/actions/network/delete-active.svg#symb">
|
<img class="clickable" alt="Delete Net {{.Net_ID}}" src="images/actions/network/delete-active.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Delete {{.Net_ID}}
|
Delete {{.Net_ID}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -402,8 +402,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -425,7 +425,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<template id="add-device-dialog">
|
<template id="add-device-dialog">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Add Expansion Card
|
Add Expansion Card
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -436,8 +436,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -448,13 +448,13 @@
|
|||||||
<p>{{.Device_ID}}</p>
|
<p>{{.Device_ID}}</p>
|
||||||
<p>{{.Device_Name}}</p>
|
<p>{{.Device_Name}}</p>
|
||||||
<div>
|
<div>
|
||||||
<device-action data-type="config" data-device="{{.Device_ID}}" data-value="{{.Device_ID}}">
|
<device-action data-type="config" 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">
|
||||||
<img class="clickable" alt="Configure Device {{.Device_ID}}" src="images/actions/device/config.svg#symb">
|
<img class="clickable" alt="Configure Device {{.Device_ID}}" src="images/actions/device/config.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Edit Expansion Card {{.Device_ID}}
|
Edit Expansion Card {{.Device_ID}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -463,20 +463,20 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</device-action>
|
</device-action>
|
||||||
<device-action data-type="delete" data-device="{{.Device_ID}}" data-value="{{.Device_ID}}">
|
<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">
|
||||||
<img class="clickable" alt="Delete Device {{.Device_ID}}" src="images/actions/device/delete-active.svg#symb">
|
<img class="clickable" alt="Delete Device {{.Device_ID}}" src="images/actions/device/delete-active.svg#symb">
|
||||||
<template id="dialog-template">
|
<template id="dialog-template">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
remove Expansion Card {{.Device_ID}}
|
remove Expansion Card {{.Device_ID}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -485,8 +485,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -516,17 +516,10 @@
|
|||||||
div.draggable-item {
|
div.draggable-item {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
div.draggable-item img {
|
div.draggable-item svg {
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
}
|
}
|
||||||
div.draggable-item p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
div.draggable-item p.volume-file {
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
#wrapper {
|
#wrapper {
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
@@ -543,18 +536,18 @@
|
|||||||
|
|
||||||
{{define "boot-target"}}
|
{{define "boot-target"}}
|
||||||
{{if .volume_id}}
|
{{if .volume_id}}
|
||||||
<div class="draggable-item" data-value="{{.volume_id}}" style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 0.5em; align-items: center;">
|
<div class="draggable-item" data-value="{{.volume_id}}" style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 10px; align-items: center;">
|
||||||
<img alt="Drag" src="images/actions/drag.svg#symb">
|
<img style="height: 1em; width: 1em" alt="Drag" src="images/actions/drag.svg#symb">
|
||||||
<img alt="Volume" src="images/resources/drive.svg#symb">
|
<img style="height: 1em; width: 1em" alt="Volume" src="images/resources/drive.svg#symb">
|
||||||
<p class="volume-id">{{.volume_id}}</p>
|
<p style="margin: 0px;">{{.volume_id}}</p>
|
||||||
<p class="volume-file">{{.file}}</p>
|
<p style="margin: 0px; overflow: hidden; white-space: nowrap;">{{.file}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{else if .net_id}}
|
{{else if .net_id}}
|
||||||
<div class="draggable-item" data-value="{{.net_id}}" style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 0.5em; align-items: center;">
|
<div class="draggable-item" data-value="{{.net_id}}" style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 10px; align-items: center;">
|
||||||
<img alt="Drag" src="images/actions/drag.svg#symb">
|
<img style="height: 1em; width: 1em" alt="Drag" src="images/actions/drag.svg#symb">
|
||||||
<img alt="Net" src="images/resources/network.svg#symb">
|
<img style="height: 1em; width: 1em" alt="Net" src="images/resources/network.svg#symb">
|
||||||
<p class="volume-id">{{.net_id}}</p>
|
<p style="margin: 0px;">{{.net_id}}</p>
|
||||||
<p class="volume-file">{{.value}}</p>
|
<p style="margin: 0px; overflow: hidden; white-space: nowrap;">{{.value}}</p>
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
margin: 0;
|
margin: 0px;
|
||||||
padding: 0;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
a img {
|
a img {
|
||||||
vertical-align: unset;
|
vertical-align: unset;
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
.row { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
.row { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
column-gap: 0.5em;
|
column-gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.nowrap { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
.nowrap { /* needed for some reason to avoid a flickering issue on chrome ONLY */
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
.hide-large {display: none !important;}
|
.hide-large {display: none !important;}
|
||||||
.hide-medium {display:none !important}
|
.hide-medium {display:none !important}
|
||||||
}
|
}
|
||||||
@media screen and (width <=601px) and (width >=440px){
|
@media screen and (width <=601px) {
|
||||||
.hide-large {display: none !important;}
|
.hide-large {display: none !important;}
|
||||||
.hide-medium {display:none !important}
|
.hide-medium {display:none !important}
|
||||||
.hide-small {display:none !important}
|
.hide-small {display:none !important}
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
<p>{{.NodeStatus}}</p>
|
<p>{{.NodeStatus}}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex row nowrap">
|
<div class="flex row nowrap" style="height: 1lh;">
|
||||||
{{if and (eq .NodeStatus "online") (eq .Status "running")}}
|
{{if and (eq .NodeStatus "online") (eq .Status "running")}}
|
||||||
<img id="power-btn" class="clickable" alt="shutdown instance" role="button" tabindex=0 src="images/actions/instance/stop.svg#symb">
|
<img id="power-btn" class="clickable" alt="shutdown instance" role="button" tabindex=0 src="images/actions/instance/stop.svg#symb">
|
||||||
<img id="configure-btn" alt="" aria-disabled="true" role="none" src="images/actions/instance/config-inactive.svg#symb">
|
<img id="configure-btn" alt="" aria-disabled="true" role="none" src="images/actions/instance/config-inactive.svg#symb">
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
{{if eq .Status "running"}}
|
{{if eq .Status "running"}}
|
||||||
Stop {{.VMID}}
|
Stop {{.VMID}}
|
||||||
{{else if eq .Status "stopped"}}
|
{{else if eq .Status "stopped"}}
|
||||||
@@ -134,8 +134,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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" >CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
<link rel="stylesheet" href="css/style.css">
|
<link rel="stylesheet" href="css/style.css">
|
||||||
<link rel="stylesheet" href="css/form.css">
|
<link rel="stylesheet" href="css/form.css">
|
||||||
<dialog class="w3-container w3-card w3-border-0">
|
<dialog class="w3-container w3-card w3-border-0">
|
||||||
<p class="w3-large" id="prompt">
|
<p class="w3-large" id="prompt" style="text-align: center;">
|
||||||
Delete {{.VMID}}
|
Delete {{.VMID}}
|
||||||
</p>
|
</p>
|
||||||
<div id="body">
|
<div id="body">
|
||||||
@@ -153,8 +153,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="controls" class="w3-center w3-container">
|
<div id="controls" class="w3-center w3-container">
|
||||||
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
|
<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">CONFIRM</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>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
{{define "pool-resources"}}
|
|
||||||
<section class="w3-card w3-padding">
|
|
||||||
<h3>Pool: {{.PoolID}}</h3>
|
|
||||||
<p id="vmid">VMID Range: {{.AllowedVMIDRange.Min}} - {{.AllowedVMIDRange.Max}}</p>
|
|
||||||
<p id="nodes">Nodes: {{MapKeys .AllowedNodes ", "}}</p>
|
|
||||||
<p id="backups">Max Backups Per Instance: {{.AllowedBackups.MaxPerInstance}} Max Backups Total: {{.AllowedBackups.MaxTotal}}</p>
|
|
||||||
<div>
|
|
||||||
{{range $category, $v := .Resources}}
|
|
||||||
{{if eq $category ""}}
|
|
||||||
<h4>Generic</h4>
|
|
||||||
{{else}}
|
|
||||||
<h4>{{$category}}</h4>
|
|
||||||
{{end}}
|
|
||||||
<div class="resource-container">
|
|
||||||
{{range $v}}
|
|
||||||
{{if .Display}}
|
|
||||||
{{if eq .Type "numeric"}}
|
|
||||||
{{template "resource-chart" .}}
|
|
||||||
{{end}}
|
|
||||||
{{if eq .Type "storage"}}
|
|
||||||
{{template "resource-chart" .}}
|
|
||||||
{{end}}
|
|
||||||
{{if eq .Type "list"}}
|
|
||||||
{{range .Resources}}
|
|
||||||
{{template "resource-chart" .}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{end}}
|
|
||||||
@@ -8,7 +8,8 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
padding: 0.5em;
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
progress {
|
progress {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
#caption {
|
#caption {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-top: 0.5em;
|
margin-top: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
<progress value="{{.Used}}" max="{{.Max}}" id="resource"></progress>
|
<progress value="{{.Used}}" max="{{.Max}}" id="resource"></progress>
|
||||||
<label id="caption" for="resource">
|
<label id="caption" for="resource">
|
||||||
<span>{{.Name}}</span>
|
<span>{{.Name}}</span>
|
||||||
<span>{{.Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
|
<span>{{printf "%g" .Avail}} {{.Prefix}}{{.Unit}} Avaliable</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,27 +1,11 @@
|
|||||||
{{/*
|
|
||||||
Select: generic data driven <select> element template
|
|
||||||
.ID = (string) select element id & name attribute
|
|
||||||
.Required = (bool) select element required attribute
|
|
||||||
.Options = ([]Options) array of Options
|
|
||||||
*/}}
|
|
||||||
{{define "select"}}
|
{{define "select"}}
|
||||||
<select class="w3-select w3-border" id="{{.ID}}" name="{{.ID}}" {{if .Required}}required{{end}}>
|
<select class="w3-select w3-border" id="{{.ID}}" name="{{.ID}}" {{if .Required}}required{{end}}>
|
||||||
{{range .Options}}
|
{{range .Options}}
|
||||||
{{template "option" .}}
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Options: generic data driven <option> element template
|
|
||||||
.Selected = (bool) option element selected attribute
|
|
||||||
.Value = (string) option element value attribute
|
|
||||||
.Display = (string) option element innerText
|
|
||||||
*/}}
|
|
||||||
{{define "option"}}
|
|
||||||
{{if .Selected}}
|
{{if .Selected}}
|
||||||
<option value="{{.Value}}" selected>{{.Display}}</option>
|
<option value="{{.Value}}" selected>{{.Display}}</option>
|
||||||
{{else}}
|
{{else}}
|
||||||
<option value="{{.Value}}">{{.Display}}</option>
|
<option value="{{.Value}}">{{.Display}}</option>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</select>
|
||||||
|
{{end}}
|
||||||
Reference in New Issue
Block a user