mirror of
https://github.com/AlchemillaHQ/Sylve.git
synced 2026-06-14 00:46:34 +03:00
npm: cleanup, jails: fix stats/graphs
This commit is contained in:
@@ -44,7 +44,7 @@ These only apply to the development version of Sylve, the production version wil
|
||||
|
||||
# Runtime Requirements
|
||||
|
||||
Sylve is designed to run on FreeBSD 14.3 or later, and it is recommended to use the latest version of FreeBSD for the best experience.
|
||||
Sylve is designed to run on FreeBSD 15.0 or later, and it is recommended to use the latest version of FreeBSD for the best experience.
|
||||
|
||||
## Dependencies
|
||||
|
||||
@@ -54,11 +54,11 @@ Running Sylve is pretty easy, but `sylve` depends on some packages that you can
|
||||
| -------------- | ------------ | -------- | -------- | ------------------------------------------------ |
|
||||
| smartmontools | 7.4_2 | No | No | Disk health monitoring |
|
||||
| tmux | 3.2 | No | No | Terminal multiplexer, used for the (web) console |
|
||||
| libvirt | 11.1.0 | No | No | Virtualization API, used for Bhyve |
|
||||
| bhyve-firmware | 1.0_2 | No | No | Collection of Firmware for bhyve |
|
||||
| samba419 | 4.19.9_9 | No | No | SMB file sharing service |
|
||||
| jansson | 2.14.1 | No | No | JSON library for C |
|
||||
| swtpm | 0.10.1 | No | No | TPM emulator for VMs |
|
||||
| libvirt | 11.7.0 | No | Yes | Virtualization API, used for Bhyve |
|
||||
| bhyve-firmware | 1.0_2 | No | Yes | Collection of Firmware for bhyve |
|
||||
| samba4XX | 4.XX | No | Yes | SMB file sharing service |
|
||||
| swtpm | 0.10.1 | No | Yes | TPM emulator for VMs |
|
||||
| jansson | 2.14.1 | No | No | C library for JSON parsing |
|
||||
|
||||
We also need to enable some services in order to run Sylve, you can drop these into `/etc/rc.conf` if you don't have it already:
|
||||
|
||||
@@ -66,15 +66,12 @@ We also need to enable some services in order to run Sylve, you can drop these i
|
||||
sysrc ntpd_enable="YES" # Optional
|
||||
sysrc ntpd_sync_on_start="YES" # Optional
|
||||
sysrc zfs_enable="YES"
|
||||
sysrc libvirtd_enable="YES"
|
||||
sysrc dnsmasq_enable="YES"
|
||||
sysrc rpcbind_enable="YES"
|
||||
sysrc nfs_server_enable="YES"
|
||||
sysrc mountd_enable="YES"
|
||||
sysrc samba_server_enable="YES"
|
||||
sysrc libvirtd_enable="YES" # Optional
|
||||
sysrc dnsmasq_enable="YES" # Optional
|
||||
sysrc samba_server_enable="YES" # Optional
|
||||
```
|
||||
|
||||
Enabling `rctl` is required. Do this by adding the following line to `/boot/loader.conf`:
|
||||
Enabling `rctl` is required if you're using Jails with Sylve. Do this by adding the following line to `/boot/loader.conf`:
|
||||
|
||||
```sh
|
||||
kern.racct.enable=1
|
||||
@@ -91,7 +88,7 @@ kern.racct.enable=1
|
||||
Install required packages.
|
||||
|
||||
```sh
|
||||
pkg install git node20 npm-node20 go tmux libvirt bhyve-firmware smartmontools tmux samba419 jansson swtpm
|
||||
pkg install git node22 npm-node22 go tmux libvirt bhyve-firmware smartmontools tmux samba422 jansson swtpm
|
||||
```
|
||||
|
||||
Clone the repo and build Sylve.
|
||||
@@ -110,12 +107,13 @@ cp -rf ../config.example.json config.json # Edit the config.json file to your li
|
||||
./sylve
|
||||
```
|
||||
|
||||
> In order to download an ISO go to:
|
||||
> Datacenter > Your Host > Utilities > Downloader > + NEW
|
||||
# Notes
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Bhyve does not support boot orders, so you cannot add installation media after creating the VM.
|
||||
> So make sure to add an installation media when creating a new VM.
|
||||
1. Bhyve doesn't support bootorders yet
|
||||
|
||||
Since Bhyve doesn't support bootorders yet, you'll need to configure the order using the UEFI boot menu. You can download
|
||||
|
||||
2. ARM64 support is still pending for Libvirt so the support is not there yet, for everything else it should just work out of the box.
|
||||
|
||||
# Contributing
|
||||
|
||||
|
||||
@@ -114,8 +114,6 @@ func SetupDatabase(cfg *internal.SylveConfig, isTest bool) *gorm.DB {
|
||||
|
||||
&clusterModels.Cluster{},
|
||||
&clusterModels.ClusterNode{},
|
||||
&clusterModels.ClusterS3Config{},
|
||||
&clusterModels.ClusterDirectoryConfig{},
|
||||
&clusterModels.ClusterOption{},
|
||||
&clusterModels.ClusterNote{},
|
||||
)
|
||||
|
||||
@@ -75,10 +75,8 @@ func (f *FSMDispatcher) Apply(l *raft.Log) any {
|
||||
|
||||
// ClusterSnapshot represents the state that will be snapshotted/restored
|
||||
type ClusterSnapshot struct {
|
||||
Notes []ClusterNote `json:"notes"`
|
||||
Options []ClusterOption `json:"options"`
|
||||
S3Configs []ClusterS3Config `json:"s3Configs"`
|
||||
DirectoryConfigs []ClusterDirectoryConfig `json:"directoryConfigs"`
|
||||
Notes []ClusterNote `json:"notes"`
|
||||
Options []ClusterOption `json:"options"`
|
||||
// We can add more tables here as needed
|
||||
}
|
||||
|
||||
@@ -92,12 +90,6 @@ func (f *FSMDispatcher) Snapshot() (raft.FSMSnapshot, error) {
|
||||
if err := f.DB.Find(&snap.Options).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.DB.Find(&snap.S3Configs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.DB.Find(&snap.DirectoryConfigs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &snap, nil
|
||||
}
|
||||
|
||||
@@ -118,8 +110,6 @@ func (f *FSMDispatcher) Restore(rc io.ReadCloser) error {
|
||||
sets := []restoreSet{
|
||||
{"cluster_notes", snap.Notes, 500},
|
||||
{"cluster_options", snap.Options, 100},
|
||||
{"cluster_s3_configs", snap.S3Configs, 100},
|
||||
{"cluster_directory_configs", snap.DirectoryConfigs, 100},
|
||||
// We can add more tables here as needed
|
||||
}
|
||||
|
||||
@@ -183,76 +173,6 @@ func RegisterDefaultHandlers(fsm *FSMDispatcher) {
|
||||
}
|
||||
})
|
||||
|
||||
fsm.Register("s3Configs", func(db *gorm.DB, action string, raw json.RawMessage) error {
|
||||
var s3Config ClusterS3Config
|
||||
switch action {
|
||||
case "create":
|
||||
if err := json.Unmarshal(raw, &s3Config); err != nil {
|
||||
return err
|
||||
}
|
||||
return upsertS3Cfg(db, &s3Config)
|
||||
case "update":
|
||||
if err := json.Unmarshal(raw, &s3Config); err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Model(&ClusterS3Config{}).
|
||||
Where("id = ?", s3Config.ID).
|
||||
Updates(s3Config).Error
|
||||
case "delete":
|
||||
var payload struct{ ID int }
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Delete(&ClusterS3Config{}, payload.ID).Error
|
||||
case "bulk_delete":
|
||||
var payload struct{ IDs []int }
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(payload.IDs) > 0 {
|
||||
return db.Delete(&ClusterS3Config{}, payload.IDs).Error
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
fsm.Register("directoryConfigs", func(db *gorm.DB, action string, raw json.RawMessage) error {
|
||||
var dirConfig ClusterDirectoryConfig
|
||||
switch action {
|
||||
case "create":
|
||||
if err := json.Unmarshal(raw, &dirConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
return upsertDirCfg(db, &dirConfig)
|
||||
case "update":
|
||||
if err := json.Unmarshal(raw, &dirConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Model(&ClusterDirectoryConfig{}).
|
||||
Where("id = ?", dirConfig.ID).
|
||||
Updates(dirConfig).Error
|
||||
case "delete":
|
||||
var payload struct{ ID int }
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Delete(&ClusterDirectoryConfig{}, payload.ID).Error
|
||||
case "bulk_delete":
|
||||
var payload struct{ IDs []int }
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(payload.IDs) > 0 {
|
||||
return db.Delete(&ClusterDirectoryConfig{}, payload.IDs).Error
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
fsm.Register("options", func(db *gorm.DB, action string, raw json.RawMessage) error {
|
||||
var opt ClusterOption
|
||||
if err := json.Unmarshal(raw, &opt); err != nil {
|
||||
|
||||
@@ -7,64 +7,3 @@
|
||||
// under sponsorship from the FreeBSD Foundation.
|
||||
|
||||
package clusterModels
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type ClusterS3Config struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
Bucket string `json:"bucket"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type ClusterDirectoryConfig struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"uniqueIndex" json:"name"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func upsertS3Cfg(db *gorm.DB, n *ClusterS3Config) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if n.ID == 0 {
|
||||
var next uint
|
||||
if err := tx.
|
||||
Table("cluster_s3_configs").
|
||||
Select("COALESCE(MAX(id), 0) + 1").
|
||||
Scan(&next).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
n.ID = next
|
||||
}
|
||||
|
||||
return tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"name", "endpoint", "region", "bucket", "access_key", "secret_key"}),
|
||||
}).Create(n).Error
|
||||
})
|
||||
}
|
||||
|
||||
func upsertDirCfg(db *gorm.DB, n *ClusterDirectoryConfig) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if n.ID == 0 {
|
||||
var next uint
|
||||
if err := tx.
|
||||
Table("cluster_directory_configs").
|
||||
Select("COALESCE(MAX(id), 0) + 1").
|
||||
Scan(&next).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
n.ID = next
|
||||
}
|
||||
|
||||
return tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"name", "path"}),
|
||||
}).Create(n).Error
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,6 +80,14 @@ type JailStats struct {
|
||||
CreatedAt time.Time `json:"createdAt" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
func (j JailStats) GetID() uint {
|
||||
return j.ID
|
||||
}
|
||||
|
||||
func (j JailStats) GetCreatedAt() time.Time {
|
||||
return j.CreatedAt
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
JailID uint `json:"jid" gorm:"column:jid;index"`
|
||||
|
||||
@@ -7,41 +7,3 @@
|
||||
// under sponsorship from the FreeBSD Foundation.
|
||||
|
||||
package clusterHandlers
|
||||
|
||||
import (
|
||||
"github.com/alchemillahq/sylve/internal"
|
||||
clusterServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/cluster"
|
||||
"github.com/alchemillahq/sylve/internal/services/cluster"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Summary Get Cluster Storages
|
||||
// @Description Get all storage backends configured in the cluster (S3, etc.)
|
||||
// @Tags Cluster
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} internal.APIResponse[clusterServiceInterfaces.Storages] "Success"
|
||||
// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error"
|
||||
// @Router /cluster/storage [get]
|
||||
func Storages(cS *cluster.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
storages, err := cS.ListStorages()
|
||||
if err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "list_storages_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[clusterServiceInterfaces.Storages]{
|
||||
Status: "success",
|
||||
Message: "storages_listed",
|
||||
Error: "",
|
||||
Data: storages,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,170 +7,3 @@
|
||||
// under sponsorship from the FreeBSD Foundation.
|
||||
|
||||
package clusterHandlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/alchemillahq/sylve/internal"
|
||||
"github.com/alchemillahq/sylve/internal/services/cluster"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hashicorp/raft"
|
||||
)
|
||||
|
||||
type CreateDirStorageRequest struct {
|
||||
Name string `json:"name" binding:"required,min=3"`
|
||||
Path string `json:"path" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary Create a Directory Storage
|
||||
// @Description Create a new Directory storage configuration in the cluster
|
||||
// @Tags Cluster
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body CreateDirStorageRequest true "Create Directory Storage Request"
|
||||
// @Success 200 {object} internal.APIResponse[any] "Success"
|
||||
// @Failure 400 {object} internal.APIResponse[any] "Bad Request"
|
||||
// @Failure 409 {object} internal.APIResponse[any] "Conflict"
|
||||
// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error"
|
||||
// @Router /cluster/storage/directory [post]
|
||||
func CreateDirStorage(cS *cluster.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if cS.Raft == nil {
|
||||
var req CreateDirStorageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "invalid_request",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := cS.ProposeDirectoryConfig(
|
||||
req.Name, req.Path, true,
|
||||
); err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "storage_create_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[any]{
|
||||
Status: "success",
|
||||
Message: "storage_created",
|
||||
Error: "",
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if cS.Raft.State() != raft.Leader {
|
||||
forwardToLeader(c, cS)
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateDirStorageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "invalid_request",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := cS.ProposeDirectoryConfig(
|
||||
req.Name, req.Path, false,
|
||||
); err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "storage_create_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[any]{
|
||||
Status: "success",
|
||||
Message: "storage_created",
|
||||
Error: "",
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Delete a Directory Storage
|
||||
// @Description Delete a Directory storage configuration from the cluster by ID
|
||||
// @Tags Cluster
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Storage ID"
|
||||
// @Success 200 {object} internal.APIResponse[any] "Success"
|
||||
// @Failure 400 {object} internal.APIResponse[any] "Bad Request"
|
||||
// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error"
|
||||
// @Router /cluster/storage/directory/{id} [delete]
|
||||
func DeleteDirStorage(cS *cluster.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
c.JSON(400, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "invalid_id",
|
||||
Error: "id must be a positive integer",
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if cS.Raft == nil {
|
||||
if err := cS.ProposeDirectoryConfigDelete(uint(id), true); err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "storage_delete_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[any]{
|
||||
Status: "success",
|
||||
Message: "storage_deleted",
|
||||
Error: "",
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if cS.Raft.State() != raft.Leader {
|
||||
forwardToLeader(c, cS)
|
||||
return
|
||||
}
|
||||
|
||||
if err := cS.ProposeDirectoryConfigDelete(uint(id), false); err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "storage_delete_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[any]{
|
||||
Status: "success",
|
||||
Message: "storage_deleted",
|
||||
Error: "",
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,175 +7,3 @@
|
||||
// under sponsorship from the FreeBSD Foundation.
|
||||
|
||||
package clusterHandlers
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/alchemillahq/sylve/internal"
|
||||
"github.com/alchemillahq/sylve/internal/services/cluster"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/hashicorp/raft"
|
||||
)
|
||||
|
||||
type CreateS3StorageRequest struct {
|
||||
Name string `json:"name" binding:"required,min=3"`
|
||||
Endpoint string `json:"endpoint" binding:"required"`
|
||||
Region string `json:"region" binding:"required"`
|
||||
Bucket string `json:"bucket" binding:"required"`
|
||||
AccessKey string `json:"accessKey" binding:"required"`
|
||||
SecretKey string `json:"secretKey" binding:"required"`
|
||||
}
|
||||
|
||||
// @Summary Create an S3 Storage
|
||||
// @Description Create a new S3 storage configuration in the cluster
|
||||
// @Tags Cluster
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body CreateS3StorageRequest true "Create S3 Storage Request"
|
||||
// @Success 200 {object} internal.APIResponse[any] "Success"
|
||||
// @Failure 400 {object} internal.APIResponse[any] "Bad Request"
|
||||
// @Failure 409 {object} internal.APIResponse[any] "Conflict"
|
||||
// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error"
|
||||
// @Router /cluster/storage/s3 [post]
|
||||
func CreateS3Storage(cS *cluster.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if cS.Raft == nil {
|
||||
var req CreateS3StorageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "invalid_request",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := cS.ProposeS3Config(
|
||||
req.Name, req.Endpoint, req.Region, req.Bucket, req.AccessKey, req.SecretKey, true,
|
||||
); err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "storage_create_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[any]{
|
||||
Status: "success",
|
||||
Message: "storage_created",
|
||||
Error: "",
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if cS.Raft.State() != raft.Leader {
|
||||
forwardToLeader(c, cS)
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateS3StorageRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "invalid_request",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := cS.ProposeS3Config(
|
||||
req.Name, req.Endpoint, req.Region, req.Bucket, req.AccessKey, req.SecretKey,
|
||||
false,
|
||||
); err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "storage_create_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[any]{
|
||||
Status: "success",
|
||||
Message: "storage_created",
|
||||
Error: "",
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Delete an S3 Storage
|
||||
// @Description Delete an S3 storage configuration from the cluster by ID
|
||||
// @Tags Cluster
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Storage ID"
|
||||
// @Success 200 {object} internal.APIResponse[any] "Success"
|
||||
// @Failure 400 {object} internal.APIResponse[any] "Bad Request"
|
||||
// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error"
|
||||
// @Router /cluster/storage/s3/{id} [delete]
|
||||
func DeleteS3Storage(cS *cluster.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
c.JSON(400, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "invalid_id",
|
||||
Error: "id must be a positive integer",
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if cS.Raft == nil {
|
||||
if err := cS.ProposeS3ConfigDelete(uint(id), true); err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "storage_delete_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[any]{
|
||||
Status: "success",
|
||||
Message: "storage_deleted",
|
||||
Error: "",
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if cS.Raft.State() != raft.Leader {
|
||||
forwardToLeader(c, cS)
|
||||
return
|
||||
}
|
||||
|
||||
if err := cS.ProposeS3ConfigDelete(uint(id), false); err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "storage_delete_failed",
|
||||
Error: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, internal.APIResponse[any]{
|
||||
Status: "success",
|
||||
Message: "storage_deleted",
|
||||
Error: "",
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/alchemillahq/sylve/internal"
|
||||
"github.com/alchemillahq/sylve/internal/db"
|
||||
jailModels "github.com/alchemillahq/sylve/internal/db/models/jail"
|
||||
jailServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/jail"
|
||||
"github.com/alchemillahq/sylve/internal/services/jail"
|
||||
@@ -165,7 +166,7 @@ func GetJailLogs(jailService *jail.Service) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// @Summary Get Jail Statistics
|
||||
// @Description Retrieve statistics for a specific jail
|
||||
// @Description Retrieve statistics for a jail by CTID and GFS step
|
||||
// @Tags Jail
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -173,26 +174,23 @@ func GetJailLogs(jailService *jail.Service) gin.HandlerFunc {
|
||||
// @Success 200 {object} internal.APIResponse[[]jailModels.JailStats] "Success"
|
||||
// @Failure 400 {object} internal.APIResponse[any] "Bad Request"
|
||||
// @Failure 500 {object} internal.APIResponse[any] "Internal Server Error"
|
||||
// @Router /jail/stats/:ctId/:limit [get]
|
||||
// @Router /jail/stats/:ctid/:step [get]
|
||||
func GetJailStats(jailService *jail.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctId := c.Param("ctId")
|
||||
limit := c.Param("limit")
|
||||
if ctId == "" || limit == "" {
|
||||
ctId, _ := utils.ParamUint(c, "ctId")
|
||||
step := c.Param("step")
|
||||
|
||||
if ctId == 0 || step == "" {
|
||||
c.JSON(400, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
Message: "invalid_request",
|
||||
Data: nil,
|
||||
Error: "ctId and limit are required",
|
||||
Error: "ctid and step are required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := jailService.GetJailUsage(
|
||||
uint(utils.StringToUint64(ctId)),
|
||||
int(utils.StringToUint64(limit)),
|
||||
)
|
||||
|
||||
stats, err := jailService.GetJailUsage(ctId, db.GFSStep(step))
|
||||
if err != nil {
|
||||
c.JSON(500, internal.APIResponse[any]{
|
||||
Status: "error",
|
||||
|
||||
@@ -327,7 +327,7 @@ func RegisterRoutes(r *gin.Engine,
|
||||
jail.GET("/:id/logs", jailHandlers.GetJailLogs(jailService))
|
||||
jail.PUT("/memory", jailHandlers.UpdateJailMemory(jailService))
|
||||
jail.PUT("/cpu", jailHandlers.UpdateJailCPU(jailService))
|
||||
jail.GET("/stats/:ctId/:limit", jailHandlers.GetJailStats(jailService))
|
||||
jail.GET("/stats/:ctId/:step", jailHandlers.GetJailStats(jailService))
|
||||
jail.PUT("/resource-limits/:ctId", jailHandlers.UpdateResourceLimits(jailService))
|
||||
|
||||
jail.POST("", jailHandlers.CreateJail(jailService))
|
||||
@@ -408,17 +408,6 @@ func RegisterRoutes(r *gin.Engine,
|
||||
clusterNotes.DELETE("/:id", clusterHandlers.DeleteNote(clusterService))
|
||||
}
|
||||
|
||||
clusterStorages := cluster.Group("/storage")
|
||||
{
|
||||
clusterStorages.GET("", clusterHandlers.Storages(clusterService))
|
||||
|
||||
clusterStorages.POST("/s3", clusterHandlers.CreateS3Storage(clusterService))
|
||||
clusterStorages.DELETE("/s3/:id", clusterHandlers.DeleteS3Storage(clusterService))
|
||||
|
||||
clusterStorages.POST("/directory", clusterHandlers.CreateDirStorage(clusterService))
|
||||
clusterStorages.DELETE("/directory/:id", clusterHandlers.DeleteDirStorage(clusterService))
|
||||
}
|
||||
|
||||
vnc := api.Group("/vnc")
|
||||
vnc.Use(EnsureCorrectHost(db))
|
||||
vnc.Use(middleware.EnsureAuthenticated(authService))
|
||||
|
||||
@@ -8,9 +8,4 @@
|
||||
|
||||
package clusterServiceInterfaces
|
||||
|
||||
import clusterModels "github.com/alchemillahq/sylve/internal/db/models/cluster"
|
||||
|
||||
type Storages struct {
|
||||
S3 []clusterModels.ClusterS3Config `json:"s3"`
|
||||
Directories []clusterModels.ClusterDirectoryConfig `json:"directories"`
|
||||
}
|
||||
type Storages struct{}
|
||||
|
||||
@@ -117,6 +117,6 @@ type EditJailNetworkRequest struct {
|
||||
|
||||
type JailServiceInterface interface {
|
||||
StoreJailUsage() error
|
||||
PruneOrphanedJailStats([]uint) error
|
||||
PruneOrphanedJailStats() error
|
||||
WatchNetworkObjectChanges() error
|
||||
}
|
||||
|
||||
@@ -172,62 +172,6 @@ func (s *Service) backfillPreClusterState() error {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var s3cfgs []clusterModels.ClusterS3Config
|
||||
if err := s.DB.Order("id ASC").Find(&s3cfgs).Error; err != nil {
|
||||
return fmt.Errorf("scan_existing_s3cfgs: %w", err)
|
||||
}
|
||||
|
||||
for _, c := range s3cfgs {
|
||||
payloadStruct := struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
Bucket string `json:"bucket"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Endpoint: c.Endpoint,
|
||||
Region: c.Region,
|
||||
Bucket: c.Bucket,
|
||||
AccessKey: c.AccessKey,
|
||||
SecretKey: c.SecretKey,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(payloadStruct)
|
||||
cmd := clusterModels.Command{Type: "s3Configs", Action: "create", Data: data}
|
||||
if err := s.Raft.Apply(utils.MustJSON(cmd), 5*time.Second).Error(); err != nil {
|
||||
return fmt.Errorf("apply_synth_create_s3cfg id=%d: %w", c.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var dircfgs []clusterModels.ClusterDirectoryConfig
|
||||
if err := s.DB.Order("id ASC").Find(&dircfgs).Error; err != nil {
|
||||
return fmt.Errorf("scan_existing_dircfgs: %w", err)
|
||||
}
|
||||
|
||||
for _, c := range dircfgs {
|
||||
payloadStruct := struct {
|
||||
ID uint `json:"id"`
|
||||
Path string `json:"path"`
|
||||
}{
|
||||
ID: c.ID,
|
||||
Path: c.Path,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(payloadStruct)
|
||||
cmd := clusterModels.Command{Type: "directoryConfigs", Action: "create", Data: data}
|
||||
if err := s.Raft.Apply(utils.MustJSON(cmd), 5*time.Second).Error(); err != nil {
|
||||
return fmt.Errorf("apply_synth_create_dircfg id=%d: %w", c.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Raft.Barrier(10 * time.Second).Error(); err != nil {
|
||||
return fmt.Errorf("barrier_after_backfill: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,150 +7,3 @@
|
||||
// under sponsorship from the FreeBSD Foundation.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
clusterModels "github.com/alchemillahq/sylve/internal/db/models/cluster"
|
||||
clusterServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/cluster"
|
||||
"github.com/alchemillahq/sylve/pkg/s3"
|
||||
)
|
||||
|
||||
func (s *Service) ListStorages() (clusterServiceInterfaces.Storages, error) {
|
||||
var s3 []clusterModels.ClusterS3Config
|
||||
var directories []clusterModels.ClusterDirectoryConfig
|
||||
|
||||
err := s.DB.Order("id ASC").Find(&s3).Error
|
||||
|
||||
if err != nil {
|
||||
return clusterServiceInterfaces.Storages{}, err
|
||||
}
|
||||
|
||||
err = s.DB.Order("id ASC").Find(&directories).Error
|
||||
if err != nil {
|
||||
return clusterServiceInterfaces.Storages{}, err
|
||||
}
|
||||
|
||||
return clusterServiceInterfaces.Storages{S3: s3, Directories: directories}, nil
|
||||
}
|
||||
|
||||
func (s *Service) ProposeS3Config(name,
|
||||
endpoint,
|
||||
region,
|
||||
bucket,
|
||||
accessKey,
|
||||
secretKey string,
|
||||
bypassRaft bool) error {
|
||||
err := s3.ValidateConfig(endpoint, region, bucket, accessKey, secretKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("s3_config_invalid: %w", err)
|
||||
}
|
||||
|
||||
if bypassRaft {
|
||||
s3 := clusterModels.ClusterS3Config{
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Region: region,
|
||||
Bucket: bucket,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
}
|
||||
|
||||
err := s.DB.Create(&s3).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.Raft == nil {
|
||||
return fmt.Errorf("raft_not_initialized")
|
||||
}
|
||||
|
||||
payloadStruct := struct {
|
||||
Name string `json:"name"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
Bucket string `json:"bucket"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}{
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Region: region,
|
||||
Bucket: bucket,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(payloadStruct)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed_to_marshal_note_payload: %w", err)
|
||||
}
|
||||
|
||||
cmd := clusterModels.Command{
|
||||
Type: "s3Configs",
|
||||
Action: "create",
|
||||
Data: data,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed_to_marshal_command: %w", err)
|
||||
}
|
||||
|
||||
applyFuture := s.Raft.Apply(payload, 5*time.Second)
|
||||
if err := applyFuture.Error(); err != nil {
|
||||
return fmt.Errorf("raft_apply_failed: %w", err)
|
||||
}
|
||||
|
||||
if resp, ok := applyFuture.Response().(error); ok && resp != nil {
|
||||
return fmt.Errorf("fsm_apply_failed: %w", resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ProposeS3ConfigDelete(id uint, bypassRaft bool) error {
|
||||
if bypassRaft {
|
||||
return s.DB.Delete(&clusterModels.ClusterS3Config{}, id).Error
|
||||
}
|
||||
|
||||
if s.Raft == nil {
|
||||
return fmt.Errorf("raft_not_initialized")
|
||||
}
|
||||
|
||||
payloadStruct := struct {
|
||||
ID uint `json:"id"`
|
||||
}{ID: id}
|
||||
|
||||
data, err := json.Marshal(payloadStruct)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed_to_marshal_delete_payload: %w", err)
|
||||
}
|
||||
|
||||
cmd := clusterModels.Command{
|
||||
Type: "s3Configs",
|
||||
Action: "delete",
|
||||
Data: data,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed_to_marshal_command: %w", err)
|
||||
}
|
||||
|
||||
applyFuture := s.Raft.Apply(payload, 5*time.Second)
|
||||
if err := applyFuture.Error(); err != nil {
|
||||
return fmt.Errorf("raft_apply_failed: %w", err)
|
||||
}
|
||||
|
||||
if resp, ok := applyFuture.Response().(error); ok && resp != nil {
|
||||
return fmt.Errorf("fsm_apply_failed: %w", resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,118 +7,3 @@
|
||||
// under sponsorship from the FreeBSD Foundation.
|
||||
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
clusterModels "github.com/alchemillahq/sylve/internal/db/models/cluster"
|
||||
"github.com/alchemillahq/sylve/pkg/utils"
|
||||
)
|
||||
|
||||
func (s *Service) ProposeDirectoryConfig(name string, path string, bypassRaft bool) error {
|
||||
exists, err := utils.IsDir(path)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check if directory exists: %w", err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return fmt.Errorf("directory does not exist: %s", path)
|
||||
}
|
||||
|
||||
if bypassRaft {
|
||||
dir := clusterModels.ClusterDirectoryConfig{
|
||||
Name: name,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
err := s.DB.Create(&dir).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.Raft == nil {
|
||||
return fmt.Errorf("raft_not_initialized")
|
||||
}
|
||||
|
||||
payloadStruct := struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
}{
|
||||
Name: name,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(payloadStruct)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed_to_marshal_note_payload: %w", err)
|
||||
}
|
||||
|
||||
cmd := clusterModels.Command{
|
||||
Type: "directoryConfigs",
|
||||
Action: "create",
|
||||
Data: data,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed_to_marshal_command: %w", err)
|
||||
}
|
||||
|
||||
applyFuture := s.Raft.Apply(payload, 5*time.Second)
|
||||
if err := applyFuture.Error(); err != nil {
|
||||
return fmt.Errorf("raft_apply_failed: %w", err)
|
||||
}
|
||||
|
||||
if resp, ok := applyFuture.Response().(error); ok && resp != nil {
|
||||
return fmt.Errorf("fsm_apply_failed: %w", resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ProposeDirectoryConfigDelete(id uint, bypassRaft bool) error {
|
||||
if bypassRaft {
|
||||
return s.DB.Delete(&clusterModels.ClusterDirectoryConfig{}, id).Error
|
||||
}
|
||||
|
||||
if s.Raft == nil {
|
||||
return fmt.Errorf("raft_not_initialized")
|
||||
}
|
||||
|
||||
payloadStruct := struct {
|
||||
ID uint `json:"id"`
|
||||
}{ID: id}
|
||||
|
||||
data, err := json.Marshal(payloadStruct)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed_to_marshal_payload: %w", err)
|
||||
}
|
||||
|
||||
cmd := clusterModels.Command{
|
||||
Type: "directoryConfigs",
|
||||
Action: "delete",
|
||||
Data: data,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed_to_marshal_command: %w", err)
|
||||
}
|
||||
|
||||
applyFuture := s.Raft.Apply(payload, 5*time.Second)
|
||||
if err := applyFuture.Error(); err != nil {
|
||||
return fmt.Errorf("raft_apply_failed: %w", err)
|
||||
}
|
||||
|
||||
if resp, ok := applyFuture.Response().(error); ok && resp != nil {
|
||||
return fmt.Errorf("fsm_apply_failed: %w", resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,9 +15,12 @@ import (
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/alchemillahq/sylve/internal/db"
|
||||
jailModels "github.com/alchemillahq/sylve/internal/db/models/jail"
|
||||
jailServiceInterfaces "github.com/alchemillahq/sylve/internal/interfaces/services/jail"
|
||||
"github.com/alchemillahq/sylve/internal/logger"
|
||||
"github.com/alchemillahq/sylve/pkg/utils"
|
||||
|
||||
cpuid "github.com/klauspost/cpuid/v2"
|
||||
@@ -170,6 +173,79 @@ func (s *Service) IsJailActive(ctId uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *Service) PruneOrphanedJailStats() error {
|
||||
if err := s.DB.
|
||||
Where(
|
||||
"jid NOT IN (?)",
|
||||
s.DB.
|
||||
Model(&jailModels.Jail{}).
|
||||
Select("id"),
|
||||
).
|
||||
Delete(&jailModels.JailStats{}).
|
||||
Error; err != nil {
|
||||
return fmt.Errorf("failed to prune orphaned JailStats: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) ApplyJailStatsRetention() error {
|
||||
var jailIDs []uint
|
||||
if err := s.DB.
|
||||
Model(&jailModels.JailStats{}).
|
||||
Select("DISTINCT jid").
|
||||
Pluck("jid", &jailIDs).Error; err != nil {
|
||||
return fmt.Errorf("failed_to_get_jail_ids_for_retention: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for _, jailID := range jailIDs {
|
||||
var stats []jailModels.JailStats
|
||||
if err := s.DB.
|
||||
Where("jid = ?", jailID).
|
||||
Order("created_at ASC").
|
||||
Find(&stats).Error; err != nil {
|
||||
return fmt.Errorf("failed_to_get_jail_stats_for_retention: %w", err)
|
||||
}
|
||||
|
||||
if len(stats) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if jail is active
|
||||
var jail jailModels.Jail
|
||||
if err := s.DB.Where("id = ?", jailID).First(&jail).Error; err != nil {
|
||||
logger.L.Error().Err(err).Uint("jail_id", jailID).Msg("failed_to_get_jail_for_retention")
|
||||
continue
|
||||
}
|
||||
|
||||
isActive, err := s.IsJailActive(jail.CTID)
|
||||
if err != nil {
|
||||
logger.L.Error().Err(err).Uint("jail_id", jailID).Msg("failed_to_check_if_jail_is_active_for_retention")
|
||||
continue
|
||||
}
|
||||
|
||||
if isActive {
|
||||
_, deleteIDs := db.ApplyGFS(now, stats)
|
||||
if len(deleteIDs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := s.DB.
|
||||
Where("id IN ?", deleteIDs).
|
||||
Delete(&jailModels.JailStats{}).Error; err != nil {
|
||||
return fmt.Errorf("failed_to_delete_old_jail_stats: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.PruneOrphanedJailStats(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) StoreJailUsage() error {
|
||||
if !s.crudMutex.TryLock() {
|
||||
return nil
|
||||
@@ -182,10 +258,8 @@ func (s *Service) StoreJailUsage() error {
|
||||
return fmt.Errorf("failed_to_load_jails: %w", err)
|
||||
}
|
||||
|
||||
jDBIDs := make([]uint, 0, len(jails))
|
||||
|
||||
if len(jails) == 0 {
|
||||
return s.PruneOrphanedJailStats(jDBIDs)
|
||||
return s.ApplyJailStatsRetention()
|
||||
}
|
||||
|
||||
states, err := s.GetStates()
|
||||
@@ -233,8 +307,11 @@ func (s *Service) StoreJailUsage() error {
|
||||
memPct = math.Round((float64(live.MemBytesUsed)/float64(sysRAM))*10000.0) / 100.0
|
||||
}
|
||||
|
||||
cpuPct = math.Max(0, math.Min(100, cpuPct))
|
||||
memPct = math.Max(0, math.Min(100, memPct))
|
||||
|
||||
stat := &jailModels.JailStats{
|
||||
JailID: (j.ID),
|
||||
JailID: j.ID,
|
||||
CPUUsage: cpuPct,
|
||||
MemoryUsage: memPct,
|
||||
}
|
||||
@@ -244,58 +321,10 @@ func (s *Service) StoreJailUsage() error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, j := range jails {
|
||||
jDBIDs = append(jDBIDs, j.ID)
|
||||
}
|
||||
|
||||
for _, dbID := range jDBIDs {
|
||||
var stats []jailModels.JailStats
|
||||
if err := s.DB.
|
||||
Where("jid = ?", dbID).
|
||||
Order("id DESC").
|
||||
Limit(256).
|
||||
Find(&stats).Error; err != nil {
|
||||
return fmt.Errorf("failed_to_get_jail_stats: %w", err)
|
||||
}
|
||||
|
||||
if len(stats) < 256 {
|
||||
continue
|
||||
}
|
||||
|
||||
cutoff := stats[len(stats)-1].ID
|
||||
if err := s.DB.
|
||||
Where("jid = ? AND id < ?", dbID, cutoff).
|
||||
Delete(&jailModels.JailStats{}).Error; err != nil {
|
||||
return fmt.Errorf("failed_to_delete_old_jail_stats: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.PruneOrphanedJailStats(jDBIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return s.ApplyJailStatsRetention()
|
||||
}
|
||||
|
||||
func (s *Service) PruneOrphanedJailStats(validJailIds []uint) error {
|
||||
if len(validJailIds) == 0 {
|
||||
return s.DB.Where("1 = 1").Delete(&jailModels.JailStats{}).Error
|
||||
}
|
||||
|
||||
valid := make([]int, len(validJailIds))
|
||||
for i, id := range validJailIds {
|
||||
valid[i] = int(id)
|
||||
}
|
||||
|
||||
if err := s.DB.
|
||||
Where("jid NOT IN ?", valid).
|
||||
Delete(&jailModels.JailStats{}).Error; err != nil {
|
||||
return fmt.Errorf("failed_to_prune_orphaned_jail_stats: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetJailUsage(ctId uint, limit int) ([]jailModels.JailStats, error) {
|
||||
func (s *Service) GetJailUsage(ctId uint, step db.GFSStep) ([]jailModels.JailStats, error) {
|
||||
var jailId uint
|
||||
if err := s.DB.Model(&jailModels.Jail{}).
|
||||
Where("ct_id = ?", ctId).
|
||||
@@ -308,15 +337,18 @@ func (s *Service) GetJailUsage(ctId uint, limit int) ([]jailModels.JailStats, er
|
||||
return nil, fmt.Errorf("jail_not_found")
|
||||
}
|
||||
|
||||
var jailStats []jailModels.JailStats
|
||||
sub := s.DB.
|
||||
Model(&jailModels.JailStats{}).
|
||||
Where("jid = ?", jailId).
|
||||
Order("id DESC").
|
||||
Limit(limit)
|
||||
window, err := step.Window()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.DB.Table("(?) as sub", sub).
|
||||
Order("id ASC").
|
||||
now := time.Now()
|
||||
from := now.Add(-window)
|
||||
|
||||
var jailStats []jailModels.JailStats
|
||||
if err := s.DB.
|
||||
Where("jid = ? AND created_at >= ?", jailId, from).
|
||||
Order("created_at ASC").
|
||||
Find(&jailStats).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed_to_get_jail_usage: %w", err)
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func validateValues(oType string, values []string) error {
|
||||
}
|
||||
|
||||
if !utils.IsValidPort(vInt) {
|
||||
return fmt.Errorf("invalid port value: %s", value)
|
||||
return fmt.Errorf("invalid port value: %d", vInt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,11 +172,11 @@ func (s *Service) IsObjectUsed(id uint) (bool, error) {
|
||||
}
|
||||
|
||||
if err := s.DB.Find(&jailNetworks).Error; err != nil {
|
||||
return true, fmt.Errorf("failed to find jail networks: %w", id, err)
|
||||
return true, fmt.Errorf("failed to find jail networks: %d %w", id, err)
|
||||
}
|
||||
|
||||
if err := s.DB.Preload("IPObject.Entries").Find(&dhcpLeases).Error; err != nil {
|
||||
return true, fmt.Errorf("failed to find DHCP leases: %w", id, err)
|
||||
return true, fmt.Errorf("failed to find DHCP leases: %d %w", id, err)
|
||||
}
|
||||
|
||||
for _, sw := range switches {
|
||||
@@ -252,7 +252,7 @@ func (s *Service) IsObjectUsed(id uint) (bool, error) {
|
||||
}
|
||||
|
||||
if err := s.DB.Preload("MACObject.Entries").Find(&dhcpLeases).Error; err != nil {
|
||||
return true, fmt.Errorf("failed to find DHCP leases: %w", id, err)
|
||||
return true, fmt.Errorf("failed to find DHCP leases: %d %w", id, err)
|
||||
}
|
||||
|
||||
for _, dl := range dhcpLeases {
|
||||
@@ -290,7 +290,7 @@ func (s *Service) IsObjectUsed(id uint) (bool, error) {
|
||||
if object.Type == "DUID" {
|
||||
var dhcpLeases []networkModels.DHCPStaticLease
|
||||
if err := s.DB.Preload("DUIDObject.Entries").Find(&dhcpLeases).Error; err != nil {
|
||||
return true, fmt.Errorf("failed to find DHCP leases: %w", id, err)
|
||||
return true, fmt.Errorf("failed to find DHCP leases: %d %w", id, err)
|
||||
}
|
||||
|
||||
for _, dl := range dhcpLeases {
|
||||
@@ -465,7 +465,7 @@ func (s *Service) EditObject(id uint, name string, oType string, values []string
|
||||
}
|
||||
|
||||
if err := s.DB.Preload("MACObject.Entries").Where("mac_object_id = ?", id).Find(&dhcpLeases).Error; err != nil {
|
||||
return fmt.Errorf("failed to find DHCP leases: %w", id, err)
|
||||
return fmt.Errorf("failed to find DHCP leases: %d %w", id, err)
|
||||
}
|
||||
|
||||
var vm vmModels.VM
|
||||
@@ -691,7 +691,7 @@ func (s *Service) EditObject(id uint, name string, oType string, values []string
|
||||
|
||||
var dhcpLeases []networkModels.DHCPStaticLease
|
||||
if err := s.DB.Preload("IPObject.Entries").Where("ip_object_id = ?", id).Find(&dhcpLeases).Error; err != nil {
|
||||
return fmt.Errorf("failed to find DHCP leases: %w", id, err)
|
||||
return fmt.Errorf("failed to find DHCP leases: %d %w", id, err)
|
||||
}
|
||||
|
||||
/* Object was used in DHCP leases, but now we're changing it to something else, we can't do that */
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestGetInt64(t *testing.T) {
|
||||
// vm.swap_idle_enabled does NOT exist on macOS
|
||||
switch runtime.GOOS {
|
||||
case "freebsd":
|
||||
key = "vm.swap_idle_enabled"
|
||||
key = "vm.kmem_zmax"
|
||||
case "darwin":
|
||||
key = "kern.maxfiles"
|
||||
}
|
||||
|
||||
Generated
-102
@@ -12,10 +12,6 @@
|
||||
"@fontsource/noto-sans": "^5.2.7",
|
||||
"@layerstack/svelte-stores": "^1.0.2",
|
||||
"@svelte-put/shortcut": "^4.1.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.90.12",
|
||||
"@tanstack/svelte-query": "^6.0.8",
|
||||
"@tanstack/svelte-query-devtools": "^6.0.0",
|
||||
"@tanstack/svelte-query-persist-client": "^6.0.10",
|
||||
"@wuchale/svelte": "^0.17.5",
|
||||
"@wuchale/vite-plugin": "^0.15.3",
|
||||
"adze": "^2.2.4",
|
||||
@@ -2027,104 +2023,6 @@
|
||||
"vite": "^5.2.0 || ^6 || ^7"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-async-storage-persister": {
|
||||
"version": "5.90.14",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.90.14.tgz",
|
||||
"integrity": "sha512-oU+u40luly1PlrOrezeVxdF7AoF6dYw7BaU16Eg0+xLBwCRbW4cutxtxWG306Pgsnks5jIRDI4DjAPZKFnPSXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.12",
|
||||
"@tanstack/query-persist-client-core": "5.91.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.90.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz",
|
||||
"integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-devtools": {
|
||||
"version": "5.91.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.91.1.tgz",
|
||||
"integrity": "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-persist-client-core": {
|
||||
"version": "5.91.11",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.91.11.tgz",
|
||||
"integrity": "sha512-NNpRGxQY/nVOdzfs5QbevPjGsUVoEiFwqxxaopLyu6todwtDOCfIOfhXSmpMVXBiCxUn7kqaUB1iwaBKqoAVRQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/svelte-query": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/svelte-query/-/svelte-query-6.0.10.tgz",
|
||||
"integrity": "sha512-J0kM3JNvRcRCM6cbHLeICs73aLp98N/nsihdVEtiNo3MEN4pAnO45qZ2yxX70MrEZ9vffXaCXMCChwgXs1lZ/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/svelte-query-devtools": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/svelte-query-devtools/-/svelte-query-devtools-6.0.2.tgz",
|
||||
"integrity": "sha512-UQdtAPsdJQ2HijMVy+W+/CHyrUZmCkubfAi/sqWsx+1fdY+g7ONa5xroBLiaebC+5jJxvk9kij61QMUHzzJgQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.91.1",
|
||||
"esm-env": "^1.2.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/svelte-query": "^6.0.8",
|
||||
"svelte": "^5.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/svelte-query-persist-client": {
|
||||
"version": "6.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/svelte-query-persist-client/-/svelte-query-persist-client-6.0.12.tgz",
|
||||
"integrity": "sha512-eBPByyOu0jkFa4GWe9vukf1tqPXbeQ78V6y/EHrYiGjfA2txnwwAf+anyOcyjMtlZFO2N2X9rEUOXelG/77Xjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-persist-client-core": "5.91.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/svelte-query": "^6.0.10",
|
||||
"svelte": "^5.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@thaunknown/thirty-two": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@thaunknown/thirty-two/-/thirty-two-1.0.5.tgz",
|
||||
|
||||
@@ -67,10 +67,6 @@
|
||||
"@fontsource/noto-sans": "^5.2.7",
|
||||
"@layerstack/svelte-stores": "^1.0.2",
|
||||
"@svelte-put/shortcut": "^4.1.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.90.12",
|
||||
"@tanstack/svelte-query": "^6.0.8",
|
||||
"@tanstack/svelte-query-devtools": "^6.0.0",
|
||||
"@tanstack/svelte-query-persist-client": "^6.0.10",
|
||||
"@wuchale/svelte": "^0.17.5",
|
||||
"@wuchale/vite-plugin": "^0.15.3",
|
||||
"adze": "^2.2.4",
|
||||
|
||||
@@ -99,8 +99,8 @@ export async function getJailLogs(id: number): Promise<JailLogs> {
|
||||
return await apiRequest(`/jail/${id}/logs`, JailLogsSchema, 'GET');
|
||||
}
|
||||
|
||||
export async function getStats(ctId: number, limit: number): Promise<JailStat[]> {
|
||||
return await apiRequest(`/jail/stats/${ctId}/${limit}`, z.array(JailStatSchema), 'GET');
|
||||
export async function getStats(ctId: number, step: string): Promise<JailStat[]> {
|
||||
return await apiRequest(`/jail/stats/${ctId}/${step}`, z.array(JailStatSchema), 'GET');
|
||||
}
|
||||
|
||||
export async function addNetwork(
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { APIResponseSchema, type APIResponse } from '$lib/types/common';
|
||||
import {
|
||||
IODelayHistoricalSchema,
|
||||
IODelaySchema,
|
||||
PoolsDiskUsageSchema,
|
||||
PoolStatPointsResponseSchema,
|
||||
ZpoolSchema,
|
||||
ZPoolStatusPoolSchema,
|
||||
type CreateZpool,
|
||||
type IODelay,
|
||||
type IODelayHistorical,
|
||||
type PoolsDiskUsage,
|
||||
type PoolStatPointsResponse,
|
||||
type ReplaceDevice,
|
||||
@@ -16,24 +12,6 @@ import {
|
||||
type ZpoolStatusPool
|
||||
} from '$lib/types/zfs/pool';
|
||||
import { apiRequest } from '$lib/utils/http';
|
||||
import type { QueryFunctionContext } from '@tanstack/svelte-query';
|
||||
|
||||
export async function getIODelay(
|
||||
queryObj: QueryFunctionContext | undefined
|
||||
): Promise<IODelay | IODelayHistorical> {
|
||||
if (queryObj) {
|
||||
if (queryObj.queryKey.includes('ioDelayHistorical')) {
|
||||
const data = await apiRequest(
|
||||
'/zfs/pool/io-delay/historical',
|
||||
IODelayHistoricalSchema,
|
||||
'GET'
|
||||
);
|
||||
return IODelayHistoricalSchema.parse(data);
|
||||
}
|
||||
}
|
||||
|
||||
return await apiRequest('/zfs/pool/io-delay', IODelaySchema, 'GET');
|
||||
}
|
||||
|
||||
export async function getPoolStatus(guid: string): Promise<ZpoolStatusPool> {
|
||||
return await apiRequest(`/zfs/pools/${guid}/status`, ZPoolStatusPoolSchema, 'GET');
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { getNodes } from '$lib/api/cluster/cluster';
|
||||
import { getJails, newJail } from '$lib/api/jail/jail';
|
||||
import { getSimpleJails, newJail } from '$lib/api/jail/jail';
|
||||
import { getNetworkObjects } from '$lib/api/network/object';
|
||||
import { getSwitches } from '$lib/api/network/switch';
|
||||
import { getDownloads } from '$lib/api/utilities/downloader';
|
||||
import { getVMs } from '$lib/api/vm/vm';
|
||||
import { getDatasets } from '$lib/api/zfs/datasets';
|
||||
import { getSimpleVMs } from '$lib/api/vm/vm';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||
import { reload } from '$lib/stores/api.svelte';
|
||||
import type { ClusterNode } from '$lib/types/cluster/cluster';
|
||||
import type { CreateData, Jail } from '$lib/types/jail/jail';
|
||||
import type { NetworkObject } from '$lib/types/network/object';
|
||||
import type { SwitchList } from '$lib/types/network/switch';
|
||||
import type { Download } from '$lib/types/utilities/downloader';
|
||||
import type { VM } from '$lib/types/vm/vm';
|
||||
import type { Dataset } from '$lib/types/zfs/dataset';
|
||||
import type { CreateData } from '$lib/types/jail/jail';
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { isValidCreateData } from '$lib/utils/jail/jail';
|
||||
import { getNextId } from '$lib/utils/vm/vm';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { resource } from 'runed';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Basic from './Basic.svelte';
|
||||
import Hardware from './Hardware.svelte';
|
||||
@@ -44,73 +36,59 @@
|
||||
{ value: 'advanced', label: 'Advanced' }
|
||||
];
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['zfs-datasets'],
|
||||
queryFn: async () => {
|
||||
return await getDatasets();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: [] as Dataset[],
|
||||
refetchOnMount: 'always'
|
||||
},
|
||||
{
|
||||
queryKey: ['downloads'],
|
||||
queryFn: async () => {
|
||||
return await getDownloads();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: [] as Download[],
|
||||
refetchOnMount: 'always'
|
||||
},
|
||||
{
|
||||
queryKey: ['network-switches'],
|
||||
queryFn: async () => {
|
||||
return await getSwitches();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: {} as SwitchList,
|
||||
refetchOnMount: 'always'
|
||||
},
|
||||
{
|
||||
queryKey: ['vm-list'],
|
||||
queryFn: async () => {
|
||||
return await getVMs();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: [] as VM[],
|
||||
refetchOnMount: 'always'
|
||||
},
|
||||
{
|
||||
queryKey: ['network-objects'],
|
||||
queryFn: async () => {
|
||||
return await getNetworkObjects();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: [] as NetworkObject[],
|
||||
refetchOnMount: 'always'
|
||||
},
|
||||
{
|
||||
queryKey: ['jail-list'],
|
||||
queryFn: async () => {
|
||||
return await getJails();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: [] as Jail[],
|
||||
refetchOnMount: 'always'
|
||||
},
|
||||
{
|
||||
queryKey: ['cluster-nodes'],
|
||||
queryFn: async () => {
|
||||
return await getNodes();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: [] as ClusterNode[],
|
||||
refetchOnMount: 'always'
|
||||
}
|
||||
]
|
||||
}));
|
||||
let downloads = resource(
|
||||
() => 'downloads',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const downloads = await getDownloads();
|
||||
updateCache(key, downloads);
|
||||
return downloads;
|
||||
}
|
||||
);
|
||||
|
||||
let networkSwitches = resource(
|
||||
() => 'network-switches',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const switches = await getSwitches();
|
||||
updateCache(key, switches);
|
||||
return switches;
|
||||
}
|
||||
);
|
||||
|
||||
let networkObjects = resource(
|
||||
() => 'network-objects',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const objects = await getNetworkObjects();
|
||||
updateCache(key, objects);
|
||||
return objects;
|
||||
}
|
||||
);
|
||||
|
||||
let vms = resource(
|
||||
() => 'simple-vm-list',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const vms = await getSimpleVMs();
|
||||
updateCache(key, vms);
|
||||
return vms;
|
||||
}
|
||||
);
|
||||
|
||||
let jails = resource(
|
||||
() => 'simple-jail-list',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const jails = await getSimpleJails();
|
||||
updateCache(key, jails);
|
||||
return jails;
|
||||
}
|
||||
);
|
||||
|
||||
let nodes = resource(
|
||||
() => 'cluster-nodes',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const nodes = await getNodes();
|
||||
updateCache(key, nodes);
|
||||
return nodes;
|
||||
}
|
||||
);
|
||||
|
||||
let pools = resource(
|
||||
() => 'pool-list',
|
||||
@@ -123,22 +101,23 @@
|
||||
|
||||
let refetch = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (refetch) {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
watch(
|
||||
() => refetch,
|
||||
(value) => {
|
||||
if (value) {
|
||||
downloads.refetch();
|
||||
networkSwitches.refetch();
|
||||
networkObjects.refetch();
|
||||
vms.refetch();
|
||||
jails.refetch();
|
||||
nodes.refetch();
|
||||
pools.refetch();
|
||||
|
||||
refetch = false;
|
||||
refetch = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let downloads = $derived(results[1].data as Download[]);
|
||||
let networkSwitches: SwitchList = $derived(results[2].data as SwitchList);
|
||||
let networkObjects = $derived(results[4].data as NetworkObject[]);
|
||||
let vms: VM[] = $derived(results[3].data as VM[]);
|
||||
let jails: Jail[] = $derived(results[5].data as Jail[]);
|
||||
let nodes: ClusterNode[] = $derived(results[6].data as ClusterNode[]);
|
||||
let creating: boolean = $state(false);
|
||||
|
||||
let options = {
|
||||
@@ -192,12 +171,22 @@
|
||||
}
|
||||
};
|
||||
|
||||
let nextId = $derived(getNextId(vms, jails));
|
||||
let nextId = $derived.by(() => {
|
||||
if (vms.current && jails.current) {
|
||||
return getNextId(vms.current, jails.current);
|
||||
}
|
||||
|
||||
return 137;
|
||||
});
|
||||
|
||||
let modal: CreateData = $state(options);
|
||||
|
||||
$effect(() => {
|
||||
modal.id = nextId;
|
||||
});
|
||||
watch(
|
||||
() => nextId,
|
||||
(id) => {
|
||||
modal.id = id;
|
||||
}
|
||||
);
|
||||
|
||||
function resetModal() {
|
||||
modal = options;
|
||||
@@ -303,7 +292,7 @@
|
||||
{#each tabs as { value, label }}
|
||||
<Tabs.Content {value}>
|
||||
<div>
|
||||
{#if value === 'basic'}
|
||||
{#if value === 'basic' && nodes.current}
|
||||
<Basic
|
||||
bind:name={modal.name}
|
||||
bind:id={modal.id}
|
||||
@@ -311,19 +300,18 @@
|
||||
bind:description={modal.description}
|
||||
bind:refetch
|
||||
bind:node={modal.node}
|
||||
{nodes}
|
||||
nodes={nodes.current}
|
||||
/>
|
||||
{:else if value === 'storage' && pools.current}
|
||||
{:else if value === 'storage' && pools.current && downloads.current && jails.current}
|
||||
<Storage
|
||||
{downloads}
|
||||
{jails}
|
||||
downloads={downloads.current}
|
||||
pools={pools.current}
|
||||
ctId={modal.id}
|
||||
bind:pool={modal.storage.pool}
|
||||
bind:base={modal.storage.base}
|
||||
bind:fstab={modal.storage.fstab}
|
||||
/>
|
||||
{:else if value === 'network'}
|
||||
{:else if value === 'network' && networkSwitches.current && networkObjects.current}
|
||||
<Network
|
||||
bind:switch={modal.network.switch}
|
||||
bind:mac={modal.network.mac}
|
||||
@@ -335,8 +323,8 @@
|
||||
bind:ipv6Gateway={modal.network.ipv6Gateway}
|
||||
bind:dhcp={modal.network.dhcp}
|
||||
bind:slaac={modal.network.slaac}
|
||||
switches={networkSwitches}
|
||||
{networkObjects}
|
||||
switches={networkSwitches.current}
|
||||
networkObjects={networkObjects.current}
|
||||
/>
|
||||
{:else if value === 'hardware'}
|
||||
<Hardware
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
pool: string;
|
||||
downloads: Download[];
|
||||
base: string;
|
||||
jails: Jail[];
|
||||
fstab: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||
import { reload } from '$lib/stores/api.svelte';
|
||||
import type { ClusterNode, NodeResource } from '$lib/types/cluster/cluster';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { default as TreeViewCluster } from './TreeViewCluster.svelte';
|
||||
import { DomainState } from '$lib/types/vm/vm';
|
||||
import { resource, useInterval, watch } from 'runed';
|
||||
|
||||
let openIds = $state(new Set<string>(['datacenter']));
|
||||
|
||||
@@ -22,29 +22,25 @@
|
||||
saveOpenIds(openIds);
|
||||
};
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['cluster-resources'],
|
||||
queryFn: async () => await getClusterResources(),
|
||||
keepPreviousData: true,
|
||||
refetchInterval: 30000,
|
||||
initialData: [] as NodeResource[],
|
||||
refetchOnMount: 'always'
|
||||
},
|
||||
{
|
||||
queryKey: ['cluster-nodes'],
|
||||
queryFn: async () => await getNodes(),
|
||||
keepPreviousData: true,
|
||||
refetchInterval: 30000,
|
||||
initialData: [] as ClusterNode[],
|
||||
refetchOnMount: 'always'
|
||||
}
|
||||
]
|
||||
}));
|
||||
const cluster = resource(
|
||||
() => 'cluster-resources',
|
||||
async () => {
|
||||
return await getClusterResources();
|
||||
},
|
||||
{
|
||||
initialValue: [] as NodeResource[]
|
||||
}
|
||||
);
|
||||
|
||||
const clusterRes = $derived((results[0]?.data as NodeResource[]) ?? []);
|
||||
const nodes = $derived((results[1]?.data as ClusterNode[]) ?? []);
|
||||
const nodes = resource(
|
||||
() => 'cluster-nodes',
|
||||
async () => {
|
||||
return await getNodes();
|
||||
},
|
||||
{
|
||||
initialValue: [] as ClusterNode[]
|
||||
}
|
||||
);
|
||||
|
||||
const tree = $derived([
|
||||
{
|
||||
@@ -52,7 +48,7 @@
|
||||
label: 'Data Center',
|
||||
icon: 'ant-design--cluster-outlined',
|
||||
href: '/datacenter',
|
||||
children: clusterRes.map((n) => {
|
||||
children: cluster.current.map((n) => {
|
||||
const nodeLabel = n.hostname || n.nodeUUID;
|
||||
let mergedChildren = [
|
||||
...(n.jails ?? []).map((j) => ({
|
||||
@@ -75,7 +71,7 @@
|
||||
}))
|
||||
].sort((a, b) => a.sortId - b.sortId);
|
||||
|
||||
const found = nodes.find((node) => node.nodeUUID === n.nodeUUID);
|
||||
const found = nodes.current.find((node) => node.nodeUUID === n.nodeUUID);
|
||||
const isActive = found && found.status === 'online';
|
||||
|
||||
return {
|
||||
@@ -102,11 +98,20 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (reload.leftPanel) {
|
||||
results[0].refetch();
|
||||
results[1].refetch();
|
||||
reload.leftPanel = false;
|
||||
watch(
|
||||
() => reload.leftPanel,
|
||||
(value) => {
|
||||
if (value) {
|
||||
cluster.refetch();
|
||||
nodes.refetch();
|
||||
reload.leftPanel = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useInterval(30000, {
|
||||
callback: () => {
|
||||
reload.leftPanel = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -106,14 +106,16 @@ export async function cachedFetch<T>(
|
||||
const now = Date.now();
|
||||
const entry = await kvStorage.getItem<T>(key);
|
||||
|
||||
if (entry) {
|
||||
console.log('cachedFetch:', { key, entry, now, duration });
|
||||
|
||||
if (entry && entry.data !== null) {
|
||||
const isFresh = now - entry.timestamp < duration;
|
||||
const data = entry.data;
|
||||
|
||||
const looksLikeError =
|
||||
data &&
|
||||
typeof data === 'object' &&
|
||||
'status' in (data as any) &&
|
||||
data !== null &&
|
||||
'status' in data &&
|
||||
(data as any).status === 'error';
|
||||
|
||||
if (isFresh && !looksLikeError) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Jail, SimpleJail } from '$lib/types/jail/jail';
|
||||
import type { CreateData, VM } from '$lib/types/vm/vm';
|
||||
import type { CreateData, SimpleVm, VM } from '$lib/types/vm/vm';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { isValidVMName } from '../string';
|
||||
import type { UTypeGroupedDownload } from '$lib/types/utilities/downloader';
|
||||
@@ -128,7 +128,7 @@ export function isValidCreateData(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getNextId(vms: VM[], jails: Jail[] | SimpleJail[]): number {
|
||||
export function getNextId(vms: VM[] | SimpleVm[], jails: Jail[] | SimpleJail[]): number {
|
||||
const usedIds = [...vms.map((vm) => vm.rid), ...jails.map((jail) => jail.ctId)];
|
||||
if (usedIds.length === 0) return 100;
|
||||
return Math.max(...usedIds) + 1;
|
||||
|
||||
+13
-7
@@ -32,7 +32,6 @@ msgstr "Console"
|
||||
#: src/lib/components/custom/VM/Create/CreateVM.svelte
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/datacenter/+layout.svelte
|
||||
msgid "Storage"
|
||||
msgstr "Storage"
|
||||
|
||||
@@ -116,9 +115,8 @@ msgstr "Explorer"
|
||||
msgid "Disks"
|
||||
msgstr "Disks"
|
||||
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
msgid "Dashboard"
|
||||
msgstr "Dashboard"
|
||||
#~ msgid "Dashboard"
|
||||
#~ msgstr "Dashboard"
|
||||
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/[node]/storage/zfs/dashboard/+page.svelte
|
||||
@@ -1055,12 +1053,14 @@ msgstr "Note"
|
||||
msgid "Post Upgrade Summary"
|
||||
msgstr "Post Upgrade Summary"
|
||||
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
msgid "Content"
|
||||
msgstr "Content"
|
||||
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
msgid "This is a note"
|
||||
msgstr "This is a note"
|
||||
@@ -1107,8 +1107,6 @@ msgstr "Failed to delete notes"
|
||||
#: src/lib/components/custom/Charts/Area.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
@@ -1171,7 +1169,7 @@ msgstr "Sylve Version"
|
||||
#: src/lib/components/custom/Charts/EChart.svelte
|
||||
#: src/lib/components/custom/Charts/EChartSample.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Memory Usage"
|
||||
msgstr "Memory Usage"
|
||||
|
||||
@@ -5395,22 +5393,27 @@ msgstr "Storage updated"
|
||||
#~ msgid "Manage ({0} lpinned)"
|
||||
#~ msgstr "Manage ({0} lpinned)"
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Hourly"
|
||||
msgstr "Hourly"
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Daily"
|
||||
msgstr "Daily"
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Weekly"
|
||||
msgstr "Weekly"
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Monthly"
|
||||
msgstr "Monthly"
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Yearly"
|
||||
msgstr "Yearly"
|
||||
@@ -6739,3 +6742,6 @@ msgstr "Manage the devfs ruleset for this jail"
|
||||
#: src/lib/components/custom/Jail/Options/TextEdit.svelte
|
||||
msgid "Manage additional options for this jail"
|
||||
msgstr "Manage additional options for this jail"
|
||||
|
||||
#~ msgid "Discord"
|
||||
#~ msgstr "Discord"
|
||||
|
||||
+13
-7
@@ -32,7 +32,6 @@ msgstr "कंसोल"
|
||||
#: src/lib/components/custom/VM/Create/CreateVM.svelte
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/datacenter/+layout.svelte
|
||||
msgid "Storage"
|
||||
msgstr "भंडारण"
|
||||
|
||||
@@ -116,9 +115,8 @@ msgstr "एक्सप्लोरर"
|
||||
msgid "Disks"
|
||||
msgstr "डिस्क्स"
|
||||
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
msgid "Dashboard"
|
||||
msgstr "डैशबोर्ड"
|
||||
#~ msgid "Dashboard"
|
||||
#~ msgstr "डैशबोर्ड"
|
||||
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/[node]/storage/zfs/dashboard/+page.svelte
|
||||
@@ -1011,12 +1009,14 @@ msgstr "नोट"
|
||||
msgid "Post Upgrade Summary"
|
||||
msgstr "पोस्ट अपग्रेड सारांश"
|
||||
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
msgid "Content"
|
||||
msgstr "सामग्री"
|
||||
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
msgid "This is a note"
|
||||
msgstr "यह एक नोट है"
|
||||
@@ -1105,8 +1105,6 @@ msgstr "क्लस्टर रीसेट के बाद लॉगिन
|
||||
#: src/lib/components/custom/Charts/Area.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
@@ -1169,7 +1167,7 @@ msgstr "Sylve संस्करण"
|
||||
#: src/lib/components/custom/Charts/EChart.svelte
|
||||
#: src/lib/components/custom/Charts/EChartSample.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Memory Usage"
|
||||
msgstr "मेमोरी उपयोग"
|
||||
|
||||
@@ -5383,22 +5381,27 @@ msgstr ""
|
||||
#~ msgid "Manage ({0} lpinned)"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Hourly"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Daily"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Weekly"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Monthly"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Yearly"
|
||||
msgstr ""
|
||||
@@ -6699,3 +6702,6 @@ msgstr ""
|
||||
#: src/lib/components/custom/Jail/Options/TextEdit.svelte
|
||||
msgid "Manage additional options for this jail"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Discord"
|
||||
#~ msgstr ""
|
||||
|
||||
+13
-7
@@ -32,7 +32,6 @@ msgstr ""
|
||||
#: src/lib/components/custom/VM/Create/CreateVM.svelte
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/datacenter/+layout.svelte
|
||||
msgid "Storage"
|
||||
msgstr ""
|
||||
|
||||
@@ -116,9 +115,8 @@ msgstr ""
|
||||
msgid "Disks"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
#~ msgid "Dashboard"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/[node]/+layout.svelte
|
||||
#: src/routes/[node]/storage/zfs/dashboard/+page.svelte
|
||||
@@ -1053,12 +1051,14 @@ msgstr ""
|
||||
msgid "Post Upgrade Summary"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
msgid "Content"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/notes/+page.svelte
|
||||
#: src/routes/datacenter/notes/+page.svelte
|
||||
msgid "This is a note"
|
||||
msgstr ""
|
||||
@@ -1105,8 +1105,6 @@ msgstr ""
|
||||
#: src/lib/components/custom/Charts/Area.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
@@ -1169,7 +1167,7 @@ msgstr ""
|
||||
#: src/lib/components/custom/Charts/EChart.svelte
|
||||
#: src/lib/components/custom/Charts/EChartSample.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
@@ -5377,22 +5375,27 @@ msgstr ""
|
||||
#~ msgid "Manage ({0} lpinned)"
|
||||
#~ msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Hourly"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Daily"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Weekly"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Monthly"
|
||||
msgstr ""
|
||||
|
||||
#: src/routes/[node]/jail/[node]/summary/+page.svelte
|
||||
#: src/routes/[node]/vm/[node]/summary/+page.svelte
|
||||
msgid "Yearly"
|
||||
msgstr ""
|
||||
@@ -6693,3 +6696,6 @@ msgstr ""
|
||||
#: src/lib/components/custom/Jail/Options/TextEdit.svelte
|
||||
msgid "Manage additional options for this jail"
|
||||
msgstr ""
|
||||
|
||||
#~ msgid "Discord"
|
||||
#~ msgstr ""
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
import { Toaster } from '$lib/components/ui/sonner/index.js';
|
||||
import '$lib/utils/i18n';
|
||||
import { addTabulatorFilters } from '$lib/utils/table';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import { onMount } from 'svelte';
|
||||
import '../locales/main.loader.svelte.js';
|
||||
@@ -27,14 +26,6 @@
|
||||
import Reboot from '$lib/components/custom/Initialization/Reboot.svelte';
|
||||
import { getBasicSettings } from '$lib/api/system/settings.js';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
enabled: browser
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let { children } = $props();
|
||||
let initialized = $state<boolean | null>(null);
|
||||
let rebooted = $state<boolean>(false);
|
||||
@@ -183,27 +174,25 @@
|
||||
{#if loading.throbber}
|
||||
<Throbber />
|
||||
{:else if storage.hostname && storage.token && !loading.throbber && !loading.login}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{#if initialized === null}
|
||||
<Throbber />
|
||||
{:else if initialized === false || rebooted === false}
|
||||
{#if !initialized}
|
||||
<div transition:fade|global={{ duration: 400 }}>
|
||||
<Initialize bind:initialized />
|
||||
</div>
|
||||
{:else if !rebooted}
|
||||
<div transition:fade|global={{ duration: 400 }}>
|
||||
<Reboot />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#if initialized === null}
|
||||
<Throbber />
|
||||
{:else if initialized === false || rebooted === false}
|
||||
{#if !initialized}
|
||||
<div transition:fade|global={{ duration: 400 }}>
|
||||
<Shell>
|
||||
{@render children()}
|
||||
</Shell>
|
||||
<Initialize bind:initialized />
|
||||
</div>
|
||||
{:else if !rebooted}
|
||||
<div transition:fade|global={{ duration: 400 }}>
|
||||
<Reboot />
|
||||
</div>
|
||||
{/if}
|
||||
</QueryClientProvider>
|
||||
{:else}
|
||||
<div transition:fade|global={{ duration: 400 }}>
|
||||
<Shell>
|
||||
{@render children()}
|
||||
</Shell>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div transition:fade|global={{ duration: 400 }}>
|
||||
<Login onLogin={handleLogin} loading={loading.login} />
|
||||
|
||||
@@ -128,11 +128,12 @@
|
||||
label: 'ZFS',
|
||||
icon: 'file-icons--openzfs',
|
||||
children: [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
icon: 'mdi--monitor-dashboard',
|
||||
href: `/${node}/storage/zfs/dashboard`
|
||||
},
|
||||
// Turned off dashboard for now
|
||||
// {
|
||||
// label: 'Dashboard',
|
||||
// icon: 'mdi--monitor-dashboard',
|
||||
// href: `/${node}/storage/zfs/dashboard`
|
||||
// },
|
||||
{ label: 'Pools', icon: 'bi--hdd-stack-fill', href: `/${node}/storage/zfs/pools` },
|
||||
{
|
||||
label: 'Datasets',
|
||||
@@ -272,7 +273,7 @@
|
||||
<Button
|
||||
size="sm"
|
||||
class="h-6"
|
||||
onclick={() => (window.location.href = 'https://github.com/AlchemillaHQ/Sylve')}
|
||||
onclick={() => window.open('https://discord.gg/bJB826JvXK', '_blank')}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[lucide--circle-help] mr-2 h-5 w-5"></span>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { getCPUInfo } from '$lib/api/info/cpu';
|
||||
import { getRAMInfo } from '$lib/api/info/ram';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
@@ -13,7 +12,6 @@
|
||||
jailAction,
|
||||
updateDescription
|
||||
} from '$lib/api/jail/jail';
|
||||
import AreaChart from '$lib/components/custom/Charts/Area.svelte';
|
||||
import LoadingDialog from '$lib/components/custom/Dialog/Loading.svelte';
|
||||
import * as AlertDialogRaw from '$lib/components/ui/alert-dialog/index.js';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
@@ -29,12 +27,14 @@
|
||||
import type { Jail, JailStat, JailState } from '$lib/types/jail/jail';
|
||||
import { sleep } from '$lib/utils';
|
||||
import { updateCache } from '$lib/utils/http';
|
||||
import { cleanStats } from '$lib/utils/jail/stats';
|
||||
import { dateToAgo } from '$lib/utils/time';
|
||||
import humanFormat from 'human-format';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { resource, useInterval, IsDocumentVisible, Debounced } from 'runed';
|
||||
import { resource, useInterval, IsDocumentVisible, Debounced, watch } from 'runed';
|
||||
import { untrack } from 'svelte';
|
||||
import type { GFSStep } from '$lib/types/common';
|
||||
import SimpleSelect from '$lib/components/custom/SimpleSelect.svelte';
|
||||
import LineBrush from '$lib/components/custom/Charts/LineBrush/Single.svelte';
|
||||
|
||||
interface Data {
|
||||
ctId: number;
|
||||
@@ -48,6 +48,7 @@
|
||||
let visible = new IsDocumentVisible();
|
||||
let { data }: { data: Data } = $props();
|
||||
let ctId = data.ctId;
|
||||
let gfsStep = $state<GFSStep>('hourly');
|
||||
|
||||
let modalState = $state({
|
||||
isDeleteOpen: false,
|
||||
@@ -98,16 +99,15 @@
|
||||
}
|
||||
);
|
||||
|
||||
const jailStats = resource(
|
||||
() => `jail-stats-${ctId}`,
|
||||
async (key, prevKey, { signal }) => {
|
||||
const result = await getStats(Number(ctId), 128);
|
||||
const stats = resource(
|
||||
[() => gfsStep],
|
||||
async ([gfsStep]) => {
|
||||
const result = await getStats(Number(data.jail.ctId), gfsStep);
|
||||
const key = `jail-stats-${gfsStep}-${data.jail.ctId}`;
|
||||
updateCache(key, result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.stats
|
||||
}
|
||||
{ initialValue: data.stats }
|
||||
);
|
||||
|
||||
const cpuInfo = resource(
|
||||
@@ -146,42 +146,25 @@
|
||||
if (visible.current) {
|
||||
jail.refetch();
|
||||
jState.refetch();
|
||||
jailStats.refetch();
|
||||
stats.refetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (visible.current) {
|
||||
untrack(() => {
|
||||
watch(
|
||||
() => visible.current,
|
||||
(isVisible) => {
|
||||
if (isVisible) {
|
||||
jail.refetch();
|
||||
jState.refetch();
|
||||
jailStats.refetch();
|
||||
});
|
||||
stats.refetch();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let showLogs = $state(false);
|
||||
let logicalCores = $derived(cpuInfo.current?.logicalCores ?? 0);
|
||||
let totalRAM = $derived(ramInfo.current?.total ?? 0);
|
||||
let cpuHistoricalData = $derived.by(() => {
|
||||
return {
|
||||
field: 'cpuUsage',
|
||||
label: 'CPU Usage',
|
||||
color: 'chart-1',
|
||||
data: cleanStats(jailStats.current, jail.current).cpu.slice(-12)
|
||||
};
|
||||
});
|
||||
|
||||
let memoryHistoricalData = $derived.by(() => {
|
||||
return {
|
||||
field: 'memoryUsage',
|
||||
label: 'Memory Usage',
|
||||
color: 'chart-2',
|
||||
data: cleanStats(jailStats.current, jail.current).memory.slice(-12)
|
||||
};
|
||||
});
|
||||
|
||||
let jailDesc = $state(jail.current.description || '');
|
||||
let debouncedDesc = new Debounced(() => jailDesc, 500);
|
||||
let lastDesc = $state('');
|
||||
@@ -315,17 +298,37 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="ml-auto flex h-full items-center">
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => (showLogs = true)}
|
||||
class="bg-muted-foreground/40 dark:bg-muted h-6 text-black hover:bg-blue-600 dark:text-white"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--file-document-outline] h-4 w-4"></span>
|
||||
<span>View Logs</span>
|
||||
</div>
|
||||
</Button>
|
||||
<div class="ml-auto flex h-full items-center gap-2">
|
||||
{#if logs.current.logs.length > 0}
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => {
|
||||
showLogs = true;
|
||||
}}
|
||||
class="bg-muted-foreground/40 dark:bg-muted h-6 text-black hover:bg-blue-600 dark:text-white"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--file-document-outline] h-4 w-4"></span>
|
||||
<span>View Logs</span>
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<SimpleSelect
|
||||
options={[
|
||||
{ label: 'Hourly', value: 'hourly' },
|
||||
{ label: 'Daily', value: 'daily' },
|
||||
{ label: 'Weekly', value: 'weekly' },
|
||||
{ label: 'Monthly', value: 'monthly' },
|
||||
{ label: 'Yearly', value: 'yearly' }
|
||||
]}
|
||||
bind:value={gfsStep}
|
||||
onChange={() => {
|
||||
stats.refetch();
|
||||
}}
|
||||
classes={{ trigger: 'h-6!' }}
|
||||
icon="icon-[mdi--calendar]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -420,26 +423,32 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 p-3">
|
||||
<AreaChart title="CPU Usage" elements={[cpuHistoricalData]} percentage={true} />
|
||||
<AreaChart title="Memory Usage" elements={[memoryHistoricalData]} percentage={true} />
|
||||
<LineBrush
|
||||
title="CPU Usage"
|
||||
points={stats.current.map((data) => ({
|
||||
date: new Date(data.createdAt).getTime(),
|
||||
value: Number(data.cpuUsage)
|
||||
}))}
|
||||
percentage={true}
|
||||
color="one"
|
||||
containerContentHeight="h-64"
|
||||
/>
|
||||
|
||||
<LineBrush
|
||||
title="Memory Usage"
|
||||
points={stats.current.map((data) => ({
|
||||
date: new Date(data.createdAt).getTime(),
|
||||
value: Number(data.memoryUsage)
|
||||
}))}
|
||||
percentage={true}
|
||||
color="two"
|
||||
containerContentHeight="h-64"
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <AlertDialog
|
||||
open={modalState.isDeleteOpen}
|
||||
customTitle={`This will delete Jail ${jail.name} (${jail.ctId})`}
|
||||
actions={{
|
||||
onConfirm: async () => {
|
||||
handleDelete();
|
||||
},
|
||||
onCancel: () => {
|
||||
modalState.isDeleteOpen = false;
|
||||
}
|
||||
}}
|
||||
></AlertDialog> -->
|
||||
|
||||
<AlertDialogRaw.Root bind:open={modalState.isDeleteOpen}>
|
||||
<AlertDialogRaw.Content onInteractOutside={(e) => e.preventDefault()} class="p-5">
|
||||
<AlertDialogRaw.Header>
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function load({ params }) {
|
||||
const [jail, state, stats, ramInfo, cpuInfo] = await Promise.all([
|
||||
cachedFetch(`jail-${ctId}`, async () => getJailById(ctId, 'ctid'), cacheDuration),
|
||||
cachedFetch(`jail-${ctId}-state`, async () => getJailStateById(ctId), cacheDuration),
|
||||
cachedFetch(`jail-${ctId}-stats`, async () => getStats(ctId, 128), cacheDuration),
|
||||
cachedFetch(`jail-${ctId}-stats`, async () => getStats(ctId, 'hourly'), cacheDuration),
|
||||
cachedFetch('system-ram-info', async () => getRAMInfo('current'), cacheDuration),
|
||||
cachedFetch('system-cpu-info', async () => getCPUInfo('current'), cacheDuration)
|
||||
]);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import type { ManualSwitch, StandardSwitch, SwitchList } from '$lib/types/network/switch';
|
||||
import { updateCache } from '$lib/utils/http';
|
||||
import { generateNanoId } from '$lib/utils/string';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { resource, watch } from 'runed';
|
||||
import type { CellComponent } from 'tabulator-tables';
|
||||
|
||||
interface Data {
|
||||
@@ -23,58 +23,49 @@
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['network-interfaces'],
|
||||
queryFn: async () => {
|
||||
return await getInterfaces();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.interfaces,
|
||||
onSuccess: (data: Iface[]) => {
|
||||
updateCache('network-interfaces', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['network-switches'],
|
||||
queryFn: async () => {
|
||||
return await getSwitches();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.switches,
|
||||
onSuccess: (data: SwitchList) => {
|
||||
updateCache('network-switches', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['dhcp-config'],
|
||||
queryFn: async () => {
|
||||
return await getDHCPConfig();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.dhcpConfig,
|
||||
onSuccess: (data: DHCPConfig) => {
|
||||
updateCache('dhcp-config', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
let networkInterfaces = resource(
|
||||
() => 'network-interfaces',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getInterfaces();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.interfaces }
|
||||
);
|
||||
|
||||
let networkSwitches = resource(
|
||||
() => 'network-switches',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getSwitches();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.switches }
|
||||
);
|
||||
|
||||
let dhcpConfig = resource(
|
||||
() => 'dhcp-config',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getDHCPConfig();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.dhcpConfig }
|
||||
);
|
||||
|
||||
let networkInterfaces = $derived(results[0].data as Iface[]);
|
||||
let networkSwitches = $derived(results[1].data as SwitchList);
|
||||
let dhcpConfig = $derived(results[2].data as DHCPConfig);
|
||||
let reload = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
|
||||
reload = false;
|
||||
watch(
|
||||
() => reload,
|
||||
(current) => {
|
||||
if (current) {
|
||||
networkInterfaces.refetch();
|
||||
networkSwitches.refetch();
|
||||
dhcpConfig.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let query = $state('');
|
||||
let tableData = $derived.by(() => {
|
||||
@@ -122,24 +113,24 @@
|
||||
{
|
||||
id: generateNanoId('id'),
|
||||
property: 'Domain',
|
||||
value: dhcpConfig.domain
|
||||
value: dhcpConfig.current.domain
|
||||
},
|
||||
{
|
||||
id: generateNanoId('expandHosts'),
|
||||
property: 'Expand Hosts',
|
||||
value: dhcpConfig.expandHosts ? 'Yes' : 'No'
|
||||
value: dhcpConfig.current.expandHosts ? 'Yes' : 'No'
|
||||
},
|
||||
{
|
||||
id: generateNanoId('dnsServers'),
|
||||
property: 'DNS Servers',
|
||||
value: dhcpConfig.dnsServers
|
||||
value: dhcpConfig.current.dnsServers
|
||||
},
|
||||
{
|
||||
id: generateNanoId('switches'),
|
||||
property: 'Switches',
|
||||
value: {
|
||||
standard: dhcpConfig.standardSwitches,
|
||||
manual: dhcpConfig.manualSwitches
|
||||
standard: dhcpConfig.current.standardSwitches,
|
||||
manual: dhcpConfig.current.manualSwitches
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -173,4 +164,10 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Config bind:open={modalOpen} bind:reload {networkInterfaces} {networkSwitches} {dhcpConfig} />
|
||||
<Config
|
||||
bind:open={modalOpen}
|
||||
bind:reload
|
||||
networkInterfaces={networkInterfaces.current}
|
||||
networkSwitches={networkSwitches.current}
|
||||
dhcpConfig={dhcpConfig.current}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import type { Iface } from '$lib/types/network/iface';
|
||||
import type { SwitchList } from '$lib/types/network/switch';
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import type { NetworkObject } from '$lib/types/network/object';
|
||||
import { getNetworkObjects } from '$lib/api/network/object';
|
||||
@@ -19,6 +18,7 @@
|
||||
import { renderWithIcon } from '$lib/utils/table';
|
||||
import AlertDialog from '$lib/components/custom/Dialog/Alert.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { resource, watch } from 'runed';
|
||||
|
||||
interface Data {
|
||||
interfaces: Iface[];
|
||||
@@ -31,94 +31,82 @@
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['network-interfaces'],
|
||||
queryFn: async () => {
|
||||
return await getInterfaces();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.interfaces,
|
||||
onSuccess: (data: Iface[]) => {
|
||||
updateCache('network-interfaces', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['network-switches'],
|
||||
queryFn: async () => {
|
||||
return await getSwitches();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.switches,
|
||||
onSuccess: (data: SwitchList) => {
|
||||
updateCache('network-switches', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['dhcp-config'],
|
||||
queryFn: async () => {
|
||||
return await getDHCPConfig();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.dhcpConfig,
|
||||
onSuccess: (data: DHCPConfig) => {
|
||||
updateCache('dhcp-config', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['dhcp-ranges'],
|
||||
queryFn: async () => {
|
||||
return await getDHCPRanges();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.dhcpRanges,
|
||||
onSuccess: (data: DHCPRange[]) => {
|
||||
updateCache('dhcp-ranges', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['dhcp-leases'],
|
||||
queryFn: async () => {
|
||||
return await getLeases();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.dhcpLeases,
|
||||
onSuccess: (data: Leases) => {
|
||||
updateCache('dhcp-leases', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['network-objects'],
|
||||
queryFn: async () => {
|
||||
return await getNetworkObjects();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.networkObjects,
|
||||
onSuccess: (data: NetworkObject[]) => {
|
||||
updateCache('network-objects', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
let networkInterfaces = resource(
|
||||
() => 'network-interfaces',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getInterfaces();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.interfaces }
|
||||
);
|
||||
|
||||
let networkSwitches = resource(
|
||||
() => 'network-switches',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getSwitches();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.switches }
|
||||
);
|
||||
|
||||
let dhcpConfig = resource(
|
||||
() => 'dhcp-config',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getDHCPConfig();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.dhcpConfig }
|
||||
);
|
||||
|
||||
let dhcpRanges = resource(
|
||||
() => 'dhcp-ranges',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getDHCPRanges();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.dhcpRanges }
|
||||
);
|
||||
|
||||
let dhcpLeases = resource(
|
||||
() => 'dhcp-leases',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getLeases();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.dhcpLeases }
|
||||
);
|
||||
|
||||
let networkObjects = resource(
|
||||
() => 'network-objects',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getNetworkObjects();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.networkObjects }
|
||||
);
|
||||
|
||||
let networkInterfaces = $derived(results[0].data as Iface[]);
|
||||
let networkSwitches = $derived(results[1].data as SwitchList);
|
||||
let dhcpConfig = $derived(results[2].data as DHCPConfig);
|
||||
let dhcpRanges = $derived(results[3].data as DHCPRange[]);
|
||||
let dhcpLeases = $derived(results[4].data as Leases);
|
||||
let networkObjects = $derived(results[5].data as NetworkObject[]);
|
||||
let reload = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
|
||||
reload = false;
|
||||
watch(
|
||||
() => reload,
|
||||
(current) => {
|
||||
if (current) {
|
||||
networkInterfaces.refetch();
|
||||
networkSwitches.refetch();
|
||||
dhcpConfig.refetch();
|
||||
dhcpRanges.refetch();
|
||||
dhcpLeases.refetch();
|
||||
networkObjects.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let modals = $state({
|
||||
create: {
|
||||
@@ -204,7 +192,7 @@
|
||||
];
|
||||
const rows: Row[] = [];
|
||||
|
||||
for (const entry of dhcpLeases.db) {
|
||||
for (const entry of dhcpLeases.current.db) {
|
||||
const range = `${entry.dhcpRange?.startIp} - ${entry.dhcpRange?.endIp}`;
|
||||
const sw = entry.dhcpRange?.standardSwitchId
|
||||
? entry.dhcpRange?.standardSwitch?.name
|
||||
@@ -229,8 +217,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
for (const entry of dhcpLeases.file) {
|
||||
const found = dhcpLeases.db.find((e) => {
|
||||
for (const entry of dhcpLeases.current.file) {
|
||||
const found = dhcpLeases.current.db.find((e) => {
|
||||
const ips = e.ipObject?.entries ? e.ipObject?.entries.map((i) => i.value) : [];
|
||||
return e.hostname === entry.hostname && ips.includes(entry.ip);
|
||||
});
|
||||
@@ -327,13 +315,13 @@
|
||||
|
||||
{#if modals.create.open}
|
||||
<CreateOrEdit
|
||||
{networkInterfaces}
|
||||
{networkSwitches}
|
||||
{dhcpConfig}
|
||||
{dhcpRanges}
|
||||
{dhcpLeases}
|
||||
networkInterfaces={networkInterfaces.current}
|
||||
networkSwitches={networkSwitches.current}
|
||||
dhcpConfig={dhcpConfig.current}
|
||||
dhcpRanges={dhcpRanges.current}
|
||||
dhcpLeases={dhcpLeases.current}
|
||||
bind:reload
|
||||
{networkObjects}
|
||||
networkObjects={networkObjects.current}
|
||||
bind:open={modals.create.open}
|
||||
selectedLease={null}
|
||||
/>
|
||||
@@ -341,13 +329,13 @@
|
||||
|
||||
{#if modals.edit.open}
|
||||
<CreateOrEdit
|
||||
{networkInterfaces}
|
||||
{networkSwitches}
|
||||
{dhcpConfig}
|
||||
{dhcpRanges}
|
||||
{dhcpLeases}
|
||||
networkInterfaces={networkInterfaces.current}
|
||||
networkSwitches={networkSwitches.current}
|
||||
dhcpConfig={dhcpConfig.current}
|
||||
dhcpRanges={dhcpRanges.current}
|
||||
dhcpLeases={dhcpLeases.current}
|
||||
bind:reload
|
||||
{networkObjects}
|
||||
networkObjects={networkObjects.current}
|
||||
bind:open={modals.edit.open}
|
||||
selectedLease={modals.edit.id}
|
||||
/>
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
import type { Column, Row } from '$lib/types/components/tree-table';
|
||||
import type { DHCPConfig, DHCPRange } from '$lib/types/network/dhcp';
|
||||
import type { Iface } from '$lib/types/network/iface';
|
||||
import type { NetworkObject } from '$lib/types/network/object';
|
||||
import type { SwitchList } from '$lib/types/network/switch';
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { secondsToDnsmasq } from '$lib/utils/string';
|
||||
import { renderWithIcon } from '$lib/utils/table';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
interface Data {
|
||||
@@ -22,73 +23,65 @@
|
||||
switches: SwitchList;
|
||||
dhcpConfig: DHCPConfig;
|
||||
dhcpRanges: DHCPRange[];
|
||||
networkObjects: NetworkObject[];
|
||||
}
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['network-interfaces'],
|
||||
queryFn: async () => {
|
||||
return await getInterfaces();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.interfaces,
|
||||
onSuccess: (data: Iface[]) => {
|
||||
updateCache('network-interfaces', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['network-switches'],
|
||||
queryFn: async () => {
|
||||
return await getSwitches();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.switches,
|
||||
onSuccess: (data: SwitchList) => {
|
||||
updateCache('network-switches', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['dhcp-config'],
|
||||
queryFn: async () => {
|
||||
return await getDHCPConfig();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.dhcpConfig,
|
||||
onSuccess: (data: DHCPConfig) => {
|
||||
updateCache('dhcp-config', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['dhcp-ranges'],
|
||||
queryFn: async () => {
|
||||
return await getDHCPRanges();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.dhcpRanges,
|
||||
onSuccess: (data: DHCPRange[]) => {
|
||||
updateCache('dhcp-ranges', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
let networkInterfaces = resource(
|
||||
() => 'network-interfaces',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getInterfaces();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.interfaces }
|
||||
);
|
||||
|
||||
let networkSwitches = resource(
|
||||
() => 'network-switches',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getSwitches();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.switches }
|
||||
);
|
||||
|
||||
let dhcpConfig = resource(
|
||||
() => 'dhcp-config',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getDHCPConfig();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.dhcpConfig }
|
||||
);
|
||||
|
||||
let dhcpRanges = resource(
|
||||
() => 'dhcp-ranges',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getDHCPRanges();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.dhcpRanges }
|
||||
);
|
||||
|
||||
let networkInterfaces = $derived(results[0].data as Iface[]);
|
||||
let networkSwitches = $derived(results[1].data as SwitchList);
|
||||
let dhcpConfig = $derived(results[2].data as DHCPConfig);
|
||||
let dhcpRanges = $derived(results[3].data as DHCPRange[]);
|
||||
let reload = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
reload = false;
|
||||
watch(
|
||||
() => reload,
|
||||
(current) => {
|
||||
if (current) {
|
||||
networkInterfaces.refetch();
|
||||
networkSwitches.refetch();
|
||||
dhcpConfig.refetch();
|
||||
dhcpRanges.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let modals = $state({
|
||||
create: {
|
||||
@@ -164,14 +157,14 @@
|
||||
|
||||
const rows: Row[] = [];
|
||||
|
||||
if (!dhcpRanges || dhcpRanges.length === 0) {
|
||||
if (!dhcpRanges || dhcpRanges.current.length === 0) {
|
||||
return {
|
||||
columns,
|
||||
rows
|
||||
};
|
||||
}
|
||||
|
||||
for (const range of dhcpRanges) {
|
||||
for (const range of dhcpRanges.current) {
|
||||
let swName = 'N/A';
|
||||
if (range.standardSwitch) {
|
||||
swName = range.standardSwitch.name;
|
||||
@@ -196,7 +189,9 @@
|
||||
let activeRow: Row | null = $derived(activeRows ? (activeRows[0] as Row) : ({} as Row));
|
||||
|
||||
let selectedRange = $derived(
|
||||
dhcpRanges && activeRow ? dhcpRanges.find((r) => r.id === Number(activeRow.id)) || null : null
|
||||
dhcpRanges && activeRow
|
||||
? dhcpRanges.current.find((r) => r.id === Number(activeRow.id)) || null
|
||||
: null
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -251,11 +246,11 @@
|
||||
<CreateOrEdit
|
||||
bind:open={modals.create.open}
|
||||
bind:reload
|
||||
{networkInterfaces}
|
||||
{networkSwitches}
|
||||
{dhcpConfig}
|
||||
networkInterfaces={networkInterfaces.current}
|
||||
networkSwitches={networkSwitches.current}
|
||||
dhcpConfig={dhcpConfig.current}
|
||||
selectedRange={null}
|
||||
{dhcpRanges}
|
||||
dhcpRanges={dhcpRanges.current}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -263,11 +258,11 @@
|
||||
<CreateOrEdit
|
||||
bind:open={modals.edit.open}
|
||||
bind:reload
|
||||
{networkInterfaces}
|
||||
{networkSwitches}
|
||||
{dhcpConfig}
|
||||
networkInterfaces={networkInterfaces.current}
|
||||
networkSwitches={networkSwitches.current}
|
||||
dhcpConfig={dhcpConfig.current}
|
||||
{selectedRange}
|
||||
{dhcpRanges}
|
||||
dhcpRanges={dhcpRanges.current}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getDHCPConfig, getDHCPRanges } from '$lib/api/network/dhcp';
|
||||
import { getInterfaces } from '$lib/api/network/iface';
|
||||
import { getSwitches } from '$lib/api/network/switch';
|
||||
import { cachedFetch } from '$lib/utils/http';
|
||||
import { getNetworkObjects } from '$lib/api/network/object';
|
||||
|
||||
export async function load() {
|
||||
const cacheDuration = 1000 * 60000;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import { updateCache } from '$lib/utils/http';
|
||||
import { generateTableData, getCleanIfaceData } from '$lib/utils/network/iface';
|
||||
import { renderWithIcon } from '$lib/utils/table';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import type { CellComponent } from 'tabulator-tables';
|
||||
import { getNetworkObjects } from '$lib/api/network/object';
|
||||
import type { NetworkObject } from '$lib/types/network/object';
|
||||
@@ -18,6 +17,7 @@
|
||||
import { isMACNearOrEqual } from '$lib/utils/mac';
|
||||
import type { VM } from '$lib/types/vm/vm';
|
||||
import { getVMs } from '$lib/api/vm/vm';
|
||||
import { resource } from 'runed';
|
||||
|
||||
interface Data {
|
||||
interfaces: Iface[];
|
||||
@@ -28,58 +28,35 @@
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['networkInterfaces'],
|
||||
queryFn: async () => {
|
||||
return await getInterfaces();
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: true,
|
||||
initialData: data.interfaces,
|
||||
onSuccess: (data: Iface[]) => {
|
||||
updateCache('networkInterfaces', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['networkObjects'],
|
||||
queryFn: async () => {
|
||||
return await getNetworkObjects();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.objects,
|
||||
onSuccess: (data: NetworkObject[]) => {
|
||||
updateCache('networkObjects', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['jail-list'],
|
||||
queryFn: async () => {
|
||||
return await getJails();
|
||||
},
|
||||
initialData: data.jails,
|
||||
keepPreviousData: true,
|
||||
refetchOnMount: 'always'
|
||||
},
|
||||
{
|
||||
queryKey: ['vm-list'],
|
||||
queryFn: async () => {
|
||||
return await getVMs();
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
initialData: data.vms,
|
||||
keepPreviousData: true,
|
||||
refetchOnMount: 'always',
|
||||
onSuccess: (data: VM[]) => {
|
||||
updateCache('vm-list', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
let networkInterfaces = resource(
|
||||
() => 'network-interfaces',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getInterfaces();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.interfaces }
|
||||
);
|
||||
|
||||
let jails = $derived(results[2].data as Jail[]);
|
||||
let vms = $derived(results[3].data as VM[]);
|
||||
let jails = resource(
|
||||
() => 'jail-list',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getJails();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.jails }
|
||||
);
|
||||
|
||||
let vms = resource(
|
||||
() => 'vm-list',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getVMs();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.vms }
|
||||
);
|
||||
|
||||
let columns: Column[] = $derived([
|
||||
{
|
||||
@@ -105,7 +82,7 @@
|
||||
}
|
||||
|
||||
if (data.isEpair) {
|
||||
const jail = jails.find((jail) =>
|
||||
const jail = jails.current.find((jail) =>
|
||||
jail?.networks?.some((net) =>
|
||||
net?.macObj?.entries?.some(
|
||||
(entry) =>
|
||||
@@ -120,7 +97,7 @@
|
||||
}
|
||||
|
||||
if (data.isTap) {
|
||||
const vm = vms.find((vm) =>
|
||||
const vm = vms.current.find((vm) =>
|
||||
vm?.networks?.some((net) =>
|
||||
net?.macObj?.entries?.some(
|
||||
(entry) =>
|
||||
@@ -201,7 +178,7 @@
|
||||
}
|
||||
]);
|
||||
|
||||
let tableData = $derived(generateTableData(columns, results[0].data as Iface[]));
|
||||
let tableData = $derived(generateTableData(columns, networkInterfaces.current));
|
||||
let activeRow: Row[] | null = $state(null);
|
||||
let query: string = $state('');
|
||||
let viewModal = $state({
|
||||
@@ -219,7 +196,7 @@
|
||||
});
|
||||
|
||||
function viewInterface(iface: string) {
|
||||
const ifaceData = results[0].data?.find((i: Iface) => i.name === iface);
|
||||
const ifaceData = networkInterfaces.current.find((i: Iface) => i.name === iface);
|
||||
if (ifaceData) {
|
||||
viewModal.KV = getCleanIfaceData(ifaceData);
|
||||
viewModal.title = `Details - ${ifaceData.name}`;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import type { SwitchList } from '$lib/types/network/switch';
|
||||
import { isAPIResponse, updateCache } from '$lib/utils/http';
|
||||
import { generateTableData } from '$lib/utils/network/switch/manual';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
interface Data {
|
||||
@@ -21,43 +21,35 @@
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['network-interfaces'],
|
||||
queryFn: async () => {
|
||||
return await getInterfaces();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.interfaces,
|
||||
onSuccess: (data: Iface[]) => {
|
||||
updateCache('network-interfaces', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['network-switches'],
|
||||
queryFn: async () => {
|
||||
return await getSwitches();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.switches,
|
||||
onSuccess: (data: SwitchList) => {
|
||||
updateCache('network-switches', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
let networkInterfaces = resource(
|
||||
() => 'network-interfaces',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getInterfaces();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.interfaces }
|
||||
);
|
||||
|
||||
let networkSwitches = resource(
|
||||
() => 'network-switches',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getSwitches();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.switches }
|
||||
);
|
||||
|
||||
const interfaces = $derived(results[0].data);
|
||||
const switches = $derived(results[1].data);
|
||||
const usable = $derived.by(() => {
|
||||
const result: string[] = [];
|
||||
const ifaces = interfaces ? interfaces.filter((iface) => iface.groups?.includes('bridge')) : [];
|
||||
const ifaces = networkInterfaces.current
|
||||
? networkInterfaces.current.filter((iface) => iface.groups?.includes('bridge'))
|
||||
: [];
|
||||
if (!ifaces.length) return [];
|
||||
|
||||
const standard = switches ? switches['standard'] || [] : [];
|
||||
const manual = switches ? switches['manual'] || [] : [];
|
||||
|
||||
const standard = networkSwitches.current ? networkSwitches.current['standard'] || [] : [];
|
||||
const manual = networkSwitches.current ? networkSwitches.current['manual'] || [] : [];
|
||||
for (const iface of ifaces) {
|
||||
const usedInStandard = standard.some((sw) => sw.bridgeName === iface.name);
|
||||
const usedInManual = manual.some((sw) => sw.bridge === iface.name);
|
||||
@@ -70,24 +62,22 @@
|
||||
return result;
|
||||
});
|
||||
|
||||
let tableData = $derived(generateTableData(switches));
|
||||
let tableData = $derived(generateTableData(networkSwitches.current));
|
||||
let activeRows: Row[] | null = $state(null);
|
||||
let activeRow: Row | null = $derived(activeRows ? (activeRows[0] as Row) : ({} as Row));
|
||||
let query: string = $state('');
|
||||
|
||||
function reloadData() {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
}
|
||||
|
||||
let reload = $state(false);
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
reloadData();
|
||||
reload = false;
|
||||
watch(
|
||||
() => reload,
|
||||
(current) => {
|
||||
if (current) {
|
||||
networkInterfaces.refetch();
|
||||
networkSwitches.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let modals = $state({
|
||||
newSwitch: {
|
||||
@@ -159,7 +149,7 @@
|
||||
actions={{
|
||||
onConfirm: async () => {
|
||||
const result = await deleteManualSwitch(modals.deleteSwitch.id);
|
||||
reloadData();
|
||||
reload = true;
|
||||
if (isAPIResponse(result) && result.status === 'success') {
|
||||
toast.success(`Switch ${modals.deleteSwitch.name} deleted`, {
|
||||
position: 'bottom-center'
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { generateTableData } from '$lib/utils/network/switch/standard';
|
||||
import { isValidMTU, isValidVLAN } from '$lib/utils/numbers';
|
||||
import { isValidSwitchName } from '$lib/utils/string';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
interface Data {
|
||||
@@ -31,54 +31,42 @@
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['network-interfaces'],
|
||||
queryFn: async () => {
|
||||
return await getInterfaces();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.interfaces,
|
||||
onSuccess: (data: Iface[]) => {
|
||||
updateCache('network-interfaces', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['network-switches'],
|
||||
queryFn: async () => {
|
||||
return await getSwitches();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.switches,
|
||||
onSuccess: (data: SwitchList) => {
|
||||
updateCache('network-switches', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['network-objects'],
|
||||
queryFn: async () => {
|
||||
return await getNetworkObjects();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.objects,
|
||||
onSuccess: (data: NetworkObject[]) => {
|
||||
updateCache('network-objects', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
const networkInterfaces = resource(
|
||||
() => 'network-interfaces',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getInterfaces();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.interfaces }
|
||||
);
|
||||
|
||||
const interfaces = $derived(results[0].data);
|
||||
const switches = $derived(results[1].data);
|
||||
const networkObjects = $derived(results[2].data);
|
||||
const switches = resource(
|
||||
() => 'network-switches',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getSwitches();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.switches }
|
||||
);
|
||||
|
||||
const networkObjects = resource(
|
||||
() => 'network-objects',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getNetworkObjects();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.objects }
|
||||
);
|
||||
|
||||
let query: string = $state('');
|
||||
let useablePorts = $derived.by(() => {
|
||||
let available: string[] = [];
|
||||
|
||||
if (interfaces) {
|
||||
for (const iface of interfaces) {
|
||||
if (networkInterfaces.current) {
|
||||
for (const iface of networkInterfaces.current) {
|
||||
available.push(iface.name);
|
||||
}
|
||||
}
|
||||
@@ -153,11 +141,19 @@
|
||||
}
|
||||
});
|
||||
|
||||
function reloadData() {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
}
|
||||
let reload = $state(false);
|
||||
|
||||
watch(
|
||||
() => reload,
|
||||
(current) => {
|
||||
if (current) {
|
||||
networkInterfaces.refetch();
|
||||
switches.refetch();
|
||||
networkObjects.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function confirmAction() {
|
||||
if (confirmModals.active === 'newSwitch' || confirmModals.active === 'editSwitch') {
|
||||
@@ -199,7 +195,7 @@
|
||||
!activeModal.dhcp &&
|
||||
activeModal.defaultRoute
|
||||
) {
|
||||
const existingSwitch = switches?.standard?.find(
|
||||
const existingSwitch = switches.current?.standard?.find(
|
||||
(sw) =>
|
||||
sw.defaultRoute && !(confirmModals.active === 'editSwitch' && sw.id === activeRow?.id)
|
||||
);
|
||||
@@ -234,7 +230,7 @@
|
||||
activeModal.defaultRoute
|
||||
);
|
||||
|
||||
reloadData();
|
||||
reload = true;
|
||||
|
||||
if (isAPIResponse(created) && created.status === 'success') {
|
||||
toast.success(`Switch ${confirmModals.newSwitch.name} created`, {
|
||||
@@ -267,7 +263,7 @@
|
||||
activeModal.defaultRoute
|
||||
);
|
||||
|
||||
reloadData();
|
||||
reload = true;
|
||||
|
||||
if (isAPIResponse(edited) && edited.status === 'success') {
|
||||
toast.success(`Switch ${confirmModals.editSwitch.name} updated`, {
|
||||
@@ -284,7 +280,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
let tableData = $derived(generateTableData(switches));
|
||||
let tableData = $derived(generateTableData(switches.current));
|
||||
let activeRows: Row[] | null = $state(null);
|
||||
let activeRow: Row | null = $derived(activeRows ? (activeRows[0] as Row) : ({} as Row));
|
||||
|
||||
@@ -548,7 +544,7 @@
|
||||
bind:open={comboBoxes.ipv4.open}
|
||||
label={'IPv4 Network'}
|
||||
bind:value={comboBoxes.ipv4.value}
|
||||
data={generateNetworkOptions(networkObjects, 'IPv4')}
|
||||
data={generateNetworkOptions(networkObjects.current, 'IPv4')}
|
||||
classes="flex-1 space-y-1"
|
||||
placeholder="Select IPv4 Network"
|
||||
width="w-3/4"
|
||||
@@ -560,7 +556,7 @@
|
||||
bind:open={comboBoxes.ipv4Gw.open}
|
||||
label={'IPv4 Gateway'}
|
||||
bind:value={comboBoxes.ipv4Gw.value}
|
||||
data={generateIPOptions(networkObjects, 'IPv4')}
|
||||
data={generateIPOptions(networkObjects.current, 'IPv4')}
|
||||
classes="flex-1 space-y-1"
|
||||
placeholder="Select IPv4 Gateway"
|
||||
width="w-3/4"
|
||||
@@ -574,7 +570,7 @@
|
||||
bind:open={comboBoxes.ipv6.open}
|
||||
label={'IPv6 Network'}
|
||||
bind:value={comboBoxes.ipv6.value}
|
||||
data={generateNetworkOptions(networkObjects, 'IPv6')}
|
||||
data={generateNetworkOptions(networkObjects.current, 'IPv6')}
|
||||
classes="flex-1 space-y-1"
|
||||
placeholder="Select IPv6 Network"
|
||||
width="w-3/4"
|
||||
@@ -589,7 +585,7 @@
|
||||
bind:open={comboBoxes.ipv6Gw.open}
|
||||
label={'IPv6 Gateway'}
|
||||
bind:value={comboBoxes.ipv6Gw.value}
|
||||
data={generateIPOptions(networkObjects, 'IPv6')}
|
||||
data={generateIPOptions(networkObjects.current, 'IPv6')}
|
||||
classes="flex-1 space-y-1"
|
||||
placeholder="Select IPv6 Gateway"
|
||||
width="w-3/4"
|
||||
@@ -682,7 +678,7 @@
|
||||
actions={{
|
||||
onConfirm: async () => {
|
||||
const result = await deleteSwitch(confirmModals.deleteSwitch.id);
|
||||
reloadData();
|
||||
reload = true;
|
||||
if (isAPIResponse(result) && result.status === 'success') {
|
||||
toast.success(`Switch ${confirmModals.deleteSwitch.name} deleted`, {
|
||||
position: 'bottom-center'
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
import { resource } from 'runed';
|
||||
import { storage } from '$lib';
|
||||
import { untrack } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { Carta, MarkdownEditor } from 'carta-md';
|
||||
import 'carta-md/default.css';
|
||||
|
||||
interface Data {
|
||||
notes: Note[];
|
||||
@@ -176,11 +173,8 @@
|
||||
let tableData = $derived(
|
||||
generateTableData(columns, notes.current && Array.isArray(notes.current) ? notes.current : [])
|
||||
);
|
||||
|
||||
let activeRow: Row[] | null = $state(null);
|
||||
let query: string = $state('');
|
||||
|
||||
const carta = new Carta();
|
||||
</script>
|
||||
|
||||
{#snippet button(type: string)}
|
||||
@@ -254,7 +248,7 @@
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<div class="flex h-full w-full flex-col" transition:fade|global={{ duration: 300 }}>
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<div class="flex h-10 w-full items-center gap-2 border-b p-2">
|
||||
<Search bind:query />
|
||||
|
||||
@@ -336,15 +330,14 @@
|
||||
|
||||
<ScrollArea orientation="vertical" class="h-full">
|
||||
{#if modalState.isEditMode}
|
||||
<div class="h-1/2">
|
||||
<!-- <CustomValueInput
|
||||
<div>
|
||||
<CustomValueInput
|
||||
label={'Content'}
|
||||
placeholder="This is a note"
|
||||
bind:value={modalState.content}
|
||||
classes="flex-1 space-y-1 "
|
||||
type="textarea"
|
||||
/> -->
|
||||
<MarkdownEditor bind:value={modalState.content} {carta} />
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-2">
|
||||
@@ -424,16 +417,3 @@
|
||||
}}
|
||||
></AlertDialog>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.carta-font-code) {
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.1rem;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.carta-editor {
|
||||
height: 10% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { convertDbTime } from '$lib/utils/time';
|
||||
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { CellComponent } from 'tabulator-tables';
|
||||
|
||||
@@ -23,39 +23,29 @@
|
||||
}
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['users'],
|
||||
queryFn: async () => {
|
||||
return (await listUsers()) as User[];
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: true,
|
||||
initialData: data.users,
|
||||
onSuccess: (data: User[]) => {
|
||||
updateCache('users', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['groups'],
|
||||
queryFn: async () => {
|
||||
return (await listGroups()) as Group[];
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: true,
|
||||
initialData: data.groups,
|
||||
onSuccess: (data: Group[]) => {
|
||||
updateCache('groups', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
let users = $derived(results[0].data as User[]);
|
||||
let groups = $derived(results[1].data as Group[]);
|
||||
const users = resource(
|
||||
() => 'users',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await listUsers();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.users }
|
||||
);
|
||||
|
||||
let groups = resource(
|
||||
() => 'groups',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await listGroups();
|
||||
updateCache(key, res);
|
||||
return res;
|
||||
},
|
||||
{ initialValue: data.groups }
|
||||
);
|
||||
|
||||
let usersOptions = $derived.by(() => {
|
||||
return users.map((user) => ({
|
||||
return users.current.map((user) => ({
|
||||
label: user.username,
|
||||
value: user.username
|
||||
}));
|
||||
@@ -86,13 +76,25 @@
|
||||
};
|
||||
|
||||
let properties = $state(options);
|
||||
let reload = $state(false);
|
||||
|
||||
watch(
|
||||
() => reload,
|
||||
(current) => {
|
||||
if (current) {
|
||||
groups.refetch();
|
||||
users.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
async function onCreate() {
|
||||
let error = '';
|
||||
|
||||
if (!properties.create.name.trim() || properties.create.users.value.length === 0) {
|
||||
error = 'Name and users are required';
|
||||
} else if (groups.some((g) => g.name === properties.create.name.trim())) {
|
||||
} else if (groups.current.some((g) => g.name === properties.create.name.trim())) {
|
||||
error = 'Group name already exists';
|
||||
}
|
||||
|
||||
@@ -108,6 +110,8 @@
|
||||
properties.create.users.value
|
||||
);
|
||||
|
||||
reload = true;
|
||||
|
||||
if (response.error) {
|
||||
handleAPIError(response);
|
||||
toast.error('Failed to create group', {
|
||||
@@ -138,6 +142,8 @@
|
||||
activeRow ? activeRow.name : ''
|
||||
);
|
||||
|
||||
reload = true;
|
||||
|
||||
if (response.status === 'error') {
|
||||
handleAPIError(response);
|
||||
toast.error('Failed to add users to group', {
|
||||
@@ -198,7 +204,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
let tableData = $derived(generateTableData(users, groups));
|
||||
let tableData = $derived(generateTableData(users.current, groups.current));
|
||||
let query: string = $state('');
|
||||
let activeRows: Row[] | null = $state(null);
|
||||
let activeRow: Row | null = $derived(activeRows ? (activeRows[0] as Row) : ({} as Row));
|
||||
@@ -277,7 +283,7 @@
|
||||
{#if properties.create.open}
|
||||
<Dialog.Root bind:open={properties.create.open}>
|
||||
<Dialog.Content
|
||||
class="sm:max-w-[425px]"
|
||||
class="sm:max-w-106.25"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeydown={(e) => e.preventDefault()}
|
||||
>
|
||||
@@ -350,7 +356,7 @@
|
||||
{#if properties.addUsers.open}
|
||||
<Dialog.Root bind:open={properties.addUsers.open}>
|
||||
<Dialog.Content
|
||||
class="sm:max-w-[425px]"
|
||||
class="sm:max-w-106.25"
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeydown={(e) => e.preventDefault()}
|
||||
>
|
||||
@@ -419,6 +425,7 @@
|
||||
actions={{
|
||||
onConfirm: async () => {
|
||||
const result = await deleteGroup(properties.delete.id);
|
||||
reload = true;
|
||||
if (result.status === 'error') {
|
||||
handleAPIError(result);
|
||||
toast.error('Failed to delete group', {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import type { Column, Row } from '$lib/types/components/tree-table';
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { convertDbTime, getLastUsage } from '$lib/utils/time';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { CellComponent } from 'tabulator-tables';
|
||||
|
||||
@@ -60,29 +60,31 @@
|
||||
return { rows, columns };
|
||||
}
|
||||
|
||||
const results = createQuery(() => ({
|
||||
queryKey: ['users'],
|
||||
queryFn: async () => {
|
||||
return (await listUsers()) as User[];
|
||||
const users = resource(
|
||||
() => 'users',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const results = await listUsers();
|
||||
updateCache('users', results);
|
||||
return results;
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.users,
|
||||
onSuccess: (data: User[]) => {
|
||||
updateCache('users', data);
|
||||
{
|
||||
initialValue: data.users
|
||||
}
|
||||
}));
|
||||
);
|
||||
|
||||
let reload = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.refetch();
|
||||
reload = false;
|
||||
watch(
|
||||
() => reload,
|
||||
(value) => {
|
||||
if (value) {
|
||||
users.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let users: User[] = $derived(results.data as User[]);
|
||||
let tableData = $derived(generateTableData(users));
|
||||
let tableData = $derived(generateTableData(users.current));
|
||||
let query: string = $state('');
|
||||
let activeRows: Row[] | null = $state(null);
|
||||
let activeRow: Row | null = $derived(activeRows ? (activeRows[0] as Row) : ({} as Row));
|
||||
@@ -103,7 +105,7 @@
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-6.5 !pointer-events-auto"
|
||||
class="h-6.5 pointer-events-auto!"
|
||||
disabled={!activeRow || activeRow.name === 'admin'}
|
||||
title={activeRow && activeRow.name === 'admin' ? 'Cannot delete admin user' : ''}
|
||||
>
|
||||
@@ -122,7 +124,7 @@
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-6.5 !pointer-events-auto"
|
||||
class="h-6.5 pointer-events-auto!"
|
||||
disabled={!activeRow || activeRow.name === 'admin'}
|
||||
title={activeRow && activeRow.name === 'admin' ? 'Cannot edit admin user' : ''}
|
||||
>
|
||||
@@ -160,15 +162,15 @@
|
||||
</div>
|
||||
|
||||
{#if modals.create.open}
|
||||
<CreateOrEdit bind:open={modals.create.open} {users} bind:reload />
|
||||
<CreateOrEdit bind:open={modals.create.open} users={users.current} bind:reload />
|
||||
{/if}
|
||||
|
||||
{#if modals.edit.open}
|
||||
<CreateOrEdit
|
||||
bind:open={modals.edit.open}
|
||||
{users}
|
||||
users={users.current}
|
||||
edit={true}
|
||||
user={activeRow ? (users.find((u) => u.id === activeRow.id) as User) : undefined}
|
||||
user={activeRow ? (users.current.find((u) => u.id === activeRow.id) as User) : undefined}
|
||||
bind:reload
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { type PCIDevice, type PPTDevice } from '$lib/types/system/pci';
|
||||
import { updateCache } from '$lib/utils/http';
|
||||
import { generateTableData } from '$lib/utils/system/pci';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
interface Data {
|
||||
@@ -17,47 +17,44 @@
|
||||
}
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['pci-devices'],
|
||||
queryFn: async () => {
|
||||
return await getPCIDevices();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.pciDevices,
|
||||
onSuccess: (data: PCIDevice[]) => {
|
||||
updateCache('pci-devices', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['ppt-devices'],
|
||||
queryFn: async () => {
|
||||
return await getPPTDevices();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.pptDevices,
|
||||
onSuccess: (data: PPTDevice[]) => {
|
||||
updateCache('ppt-devices', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
let pptDevices = resource(
|
||||
() => 'ppt-devices',
|
||||
async () => {
|
||||
const result = await getPPTDevices();
|
||||
updateCache('ppt-devices', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.pptDevices
|
||||
}
|
||||
);
|
||||
|
||||
let pciDevices = resource(
|
||||
() => 'pci-devices',
|
||||
async () => {
|
||||
const result = await getPCIDevices();
|
||||
updateCache('pci-devices', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.pciDevices
|
||||
}
|
||||
);
|
||||
|
||||
let reload = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
reload = false;
|
||||
watch(
|
||||
() => reload,
|
||||
(value) => {
|
||||
if (value) {
|
||||
pciDevices.refetch();
|
||||
pptDevices.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let pciDevices: PCIDevice[] = $derived(results[0].data as PCIDevice[]);
|
||||
let pptDevices: PPTDevice[] = $derived(results[1].data as PPTDevice[]);
|
||||
let tableData = $derived(generateTableData(pciDevices, pptDevices));
|
||||
let tableData = $derived(generateTableData(pciDevices.current, pptDevices.current));
|
||||
let tableName: string = 'device-passthrough-tt';
|
||||
let query: string = $state('');
|
||||
let activeRow: Row[] | null = $state(null);
|
||||
@@ -181,7 +178,12 @@
|
||||
});
|
||||
} else {
|
||||
let message = '';
|
||||
if (result.error?.endsWith('in_use_by_vm')) {
|
||||
if (
|
||||
typeof result.error === 'string'
|
||||
? result.error.endsWith('in_use_by_vm')
|
||||
: Array.isArray(result.error) &&
|
||||
result.error.some((e) => typeof e === 'string' && e.endsWith('in_use_by_vm'))
|
||||
) {
|
||||
message = 'Device is in use by a VM, failed to remove';
|
||||
} else {
|
||||
message = 'Failed to remove device from passthrough';
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
import type { SambaConfig } from '$lib/types/samba/config';
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { generateNanoId } from '$lib/utils/string';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { untrack } from 'svelte';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { CellComponent } from 'tabulator-tables';
|
||||
|
||||
@@ -21,52 +20,46 @@
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['samba-config'],
|
||||
queryFn: async () => {
|
||||
return await getSambaConfig();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.sambaConfig,
|
||||
onSuccess: (data: SambaConfig) => {
|
||||
updateCache('samba-config', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['network-interfaces'],
|
||||
queryFn: async () => {
|
||||
return await getInterfaces();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.interfaces,
|
||||
onSuccess: (data: Iface[]) => {
|
||||
updateCache('network-interfaces', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
let sambaConfig = resource(
|
||||
() => 'samba-config',
|
||||
async () => {
|
||||
const result = await getSambaConfig();
|
||||
updateCache('samba-config', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.sambaConfig
|
||||
}
|
||||
);
|
||||
|
||||
let networkInterfaces = resource(
|
||||
() => 'network-interfaces',
|
||||
async () => {
|
||||
const result = await getInterfaces();
|
||||
updateCache('network-interfaces', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.interfaces
|
||||
}
|
||||
);
|
||||
|
||||
let reload = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
|
||||
untrack(() => {
|
||||
watch(
|
||||
() => reload,
|
||||
(value) => {
|
||||
if (value) {
|
||||
sambaConfig.refetch();
|
||||
networkInterfaces.refetch();
|
||||
reload = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let sambaConfig: SambaConfig = $derived(results[0].data as SambaConfig);
|
||||
let interfaces: Iface[] = $derived(results[1].data as Iface[]);
|
||||
let usableIfaces = $derived.by(() => {
|
||||
let filtered = [];
|
||||
for (const iface of interfaces) {
|
||||
for (const iface of networkInterfaces.current) {
|
||||
if (iface.groups && iface.groups.length > 0) {
|
||||
if (!iface.groups.includes('tap')) {
|
||||
filtered.push(iface);
|
||||
@@ -81,23 +74,23 @@
|
||||
|
||||
let options = {
|
||||
unixCharset: {
|
||||
value: (() => $state.snapshot(sambaConfig.unixCharset))(),
|
||||
value: (() => $state.snapshot(sambaConfig.current.unixCharset))(),
|
||||
open: false
|
||||
},
|
||||
workgroup: {
|
||||
value: (() => $state.snapshot(sambaConfig.workgroup))(),
|
||||
value: (() => $state.snapshot(sambaConfig.current.workgroup))(),
|
||||
open: false
|
||||
},
|
||||
serverString: {
|
||||
value: (() => $state.snapshot(sambaConfig.serverString))(),
|
||||
value: (() => $state.snapshot(sambaConfig.current.serverString))(),
|
||||
open: false
|
||||
},
|
||||
interfaces: {
|
||||
value: (() => $state.snapshot(sambaConfig.interfaces))(),
|
||||
value: (() => $state.snapshot(sambaConfig.current.interfaces))(),
|
||||
open: false
|
||||
},
|
||||
bindInterfaces: {
|
||||
value: (() => ($state.snapshot(sambaConfig.bindInterfacesOnly) ? 'Yes' : 'No'))(),
|
||||
value: (() => ($state.snapshot(sambaConfig.current.bindInterfacesOnly) ? 'Yes' : 'No'))(),
|
||||
open: false
|
||||
}
|
||||
};
|
||||
@@ -139,29 +132,29 @@
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
id: generateNanoId(`${sambaConfig.unixCharset}`),
|
||||
id: generateNanoId(`${sambaConfig.current.unixCharset}`),
|
||||
property: 'Unix Charset',
|
||||
value: sambaConfig.unixCharset
|
||||
value: sambaConfig.current.unixCharset
|
||||
},
|
||||
{
|
||||
id: generateNanoId(`${sambaConfig.workgroup}`),
|
||||
id: generateNanoId(`${sambaConfig.current.workgroup}`),
|
||||
property: 'Workgroup',
|
||||
value: sambaConfig.workgroup
|
||||
value: sambaConfig.current.workgroup
|
||||
},
|
||||
{
|
||||
id: generateNanoId(`${sambaConfig.serverString}`),
|
||||
id: generateNanoId(`${sambaConfig.current.serverString}`),
|
||||
property: 'Server String',
|
||||
value: sambaConfig.serverString
|
||||
value: sambaConfig.current.serverString
|
||||
},
|
||||
{
|
||||
id: generateNanoId(`${sambaConfig.interfaces}`),
|
||||
id: generateNanoId(`${sambaConfig.current.interfaces}`),
|
||||
property: 'Interfaces',
|
||||
value: sambaConfig.interfaces
|
||||
value: sambaConfig.current.interfaces
|
||||
},
|
||||
{
|
||||
id: generateNanoId(`${sambaConfig.bindInterfacesOnly}`),
|
||||
id: generateNanoId(`${sambaConfig.current.bindInterfacesOnly}`),
|
||||
property: 'Bind Interfaces Only',
|
||||
value: sambaConfig.bindInterfacesOnly ? 'Yes' : 'No'
|
||||
value: sambaConfig.current.bindInterfacesOnly ? 'Yes' : 'No'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -10,11 +10,10 @@
|
||||
import type { Group } from '$lib/types/auth';
|
||||
import type { Column, Row } from '$lib/types/components/tree-table';
|
||||
import type { SambaShare } from '$lib/types/samba/shares';
|
||||
import type { Dataset } from '$lib/types/zfs/dataset';
|
||||
import { GZFSDatasetTypeSchema, type Dataset } from '$lib/types/zfs/dataset';
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { convertDbTime } from '$lib/utils/time';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import { untrack } from 'svelte';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { CellComponent } from 'tabulator-tables';
|
||||
|
||||
@@ -26,61 +25,56 @@
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['zfs-datasets'],
|
||||
queryFn: async () => {
|
||||
return await getDatasets();
|
||||
},
|
||||
keepPreviousData: false,
|
||||
initialData: data.datasets,
|
||||
onSuccess: (data: Dataset[]) => {
|
||||
updateCache('zfs-datasets', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['samba-shares'],
|
||||
queryFn: async () => {
|
||||
return await getSambaShares();
|
||||
},
|
||||
keepPreviousData: false,
|
||||
initialData: data.shares,
|
||||
onSuccess: (data: SambaShare[]) => {
|
||||
updateCache('samba-shares', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['groups'],
|
||||
queryFn: async () => {
|
||||
return (await listGroups()) as Group[];
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.groups,
|
||||
onSuccess: (data: Group[]) => {
|
||||
updateCache('groups', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
let datasets = resource(
|
||||
() => 'zfs-filesystems',
|
||||
async () => {
|
||||
const result = await getDatasets(GZFSDatasetTypeSchema.enum.FILESYSTEM);
|
||||
updateCache('zfs-filesystems', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.datasets
|
||||
}
|
||||
);
|
||||
|
||||
let shares = resource(
|
||||
() => 'samba-shares',
|
||||
async () => {
|
||||
const result = await getSambaShares();
|
||||
updateCache('samba-shares', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.shares
|
||||
}
|
||||
);
|
||||
|
||||
let groups = resource(
|
||||
() => 'groups',
|
||||
async () => {
|
||||
const result = await listGroups();
|
||||
updateCache('groups', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.groups
|
||||
}
|
||||
);
|
||||
|
||||
let reload = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.forEach((result) => {
|
||||
result.refetch();
|
||||
});
|
||||
|
||||
untrack(() => {
|
||||
watch(
|
||||
() => reload,
|
||||
(value) => {
|
||||
if (value) {
|
||||
datasets.refetch();
|
||||
shares.refetch();
|
||||
groups.refetch();
|
||||
reload = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let datasets: Dataset[] = $derived(results[0].data as Dataset[]);
|
||||
let shares: SambaShare[] = $derived(results[1].data as SambaShare[]);
|
||||
let groups: Group[] = $derived(results[2].data as Group[]);
|
||||
let activeRows: Row[] | null = $state(null);
|
||||
let activeRow: Row | null = $derived(activeRows ? (activeRows[0] as Row) : ({} as Row));
|
||||
|
||||
@@ -174,7 +168,7 @@
|
||||
};
|
||||
}
|
||||
|
||||
let tableData = $derived(generateTableData(shares, datasets, groups));
|
||||
let tableData = $derived(generateTableData(shares.current, datasets.current, groups.current));
|
||||
</script>
|
||||
|
||||
{#snippet button(type: string)}
|
||||
@@ -184,7 +178,7 @@
|
||||
onclick={() => {
|
||||
properties.edit.open = true;
|
||||
properties.edit.share =
|
||||
shares.find((share) => share.id === Number(activeRow?.id)) || null;
|
||||
shares.current.find((share) => share.id === Number(activeRow?.id)) || null;
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -249,15 +243,21 @@
|
||||
</div>
|
||||
|
||||
{#if properties.create.open}
|
||||
<Share bind:open={properties.create.open} {shares} {datasets} {groups} bind:reload />
|
||||
<Share
|
||||
bind:open={properties.create.open}
|
||||
shares={shares.current}
|
||||
datasets={datasets.current}
|
||||
groups={groups.current}
|
||||
bind:reload
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if properties.edit.open}
|
||||
<Share
|
||||
bind:open={properties.edit.open}
|
||||
{shares}
|
||||
{datasets}
|
||||
{groups}
|
||||
shares={shares.current}
|
||||
datasets={datasets.current}
|
||||
groups={groups.current}
|
||||
share={properties.edit.share}
|
||||
edit={properties.edit.open}
|
||||
bind:reload
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { listGroups } from '$lib/api/auth/groups';
|
||||
import { getInterfaces } from '$lib/api/network/iface';
|
||||
import { getSambaConfig } from '$lib/api/samba/config';
|
||||
import { getSambaShares } from '$lib/api/samba/share';
|
||||
import { getDatasets } from '$lib/api/zfs/datasets';
|
||||
import { GZFSDatasetTypeSchema } from '$lib/types/zfs/dataset';
|
||||
import { SEVEN_DAYS } from '$lib/utils';
|
||||
import { cachedFetch } from '$lib/utils/http';
|
||||
|
||||
export async function load() {
|
||||
const cacheDuration = SEVEN_DAYS;
|
||||
const [datasets, shares, groups] = await Promise.all([
|
||||
cachedFetch('zfs-datasets', async () => await getDatasets(), cacheDuration),
|
||||
cachedFetch(
|
||||
'zfs-filesystems',
|
||||
async () => await getDatasets(GZFSDatasetTypeSchema.enum.FILESYSTEM),
|
||||
cacheDuration
|
||||
),
|
||||
cachedFetch('samba-shares', async () => await getSambaShares(), cacheDuration),
|
||||
cachedFetch('groups', async () => await listGroups(), cacheDuration)
|
||||
]);
|
||||
|
||||
@@ -1,412 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getDatasets } from '$lib/api/zfs/datasets';
|
||||
import { getPools, getPoolStats } from '$lib/api/zfs/pool';
|
||||
import AreaChart from '$lib/components/custom/Charts/Area.svelte';
|
||||
import BarChart from '$lib/components/custom/Charts/Bar.svelte';
|
||||
import LineGraph from '$lib/components/custom/Charts/LineGraph.svelte';
|
||||
import PieChart from '$lib/components/custom/Charts/Pie.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import CustomComboBox from '$lib/components/ui/custom-input/combobox.svelte';
|
||||
import type { Dataset } from '$lib/types/zfs/dataset';
|
||||
import type { PoolStatPointsResponse, Zpool } from '$lib/types/zfs/pool';
|
||||
import { updateCache } from '$lib/utils/http';
|
||||
import {
|
||||
getDatasetCompressionHist,
|
||||
getPoolStatsCombined,
|
||||
getPoolUsagePieData,
|
||||
type StatType
|
||||
} from '$lib/utils/zfs/pool';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import type { Chart } from 'chart.js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Data {
|
||||
pools: Zpool[];
|
||||
datasets: Dataset[];
|
||||
poolStats: PoolStatPointsResponse;
|
||||
}
|
||||
|
||||
type CardType = 'pools' | 'datasets' | 'file_systems' | 'volumes' | 'snapshots';
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
let poolStatsRef: Chart | null = $state(null);
|
||||
let datasetChartRef: Chart | null = $state(null);
|
||||
let poolStatsInterval = $state('1');
|
||||
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['poolList'],
|
||||
queryFn: async () => {
|
||||
return await getPools();
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: false,
|
||||
initialData: data.pools,
|
||||
onSuccess: (data: Zpool[]) => {
|
||||
updateCache('pools', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['datasetList'],
|
||||
queryFn: async () => {
|
||||
return await getDatasets();
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: false,
|
||||
initialData: data.datasets,
|
||||
onSuccess: (data: Dataset[]) => {
|
||||
updateCache('datasets', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['pool-stats', poolStatsInterval],
|
||||
queryFn: async () => {
|
||||
return await getPoolStats(Number(poolStatsInterval), 128);
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: false,
|
||||
initialData: Array.isArray(data.poolStats) ? data.poolStats[0] : data.poolStats,
|
||||
onSuccess: (data: PoolStatPointsResponse) => {
|
||||
updateCache('pool-stats', data);
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
let pools: Zpool[] = $derived(results[0].data as Zpool[]);
|
||||
let datasets: Dataset[] = $derived(results[1].data as Dataset[]);
|
||||
let poolStats: PoolStatPointsResponse = $derived(results[2].data as PoolStatPointsResponse);
|
||||
|
||||
let filesystems: Dataset[] = $derived.by(() => {
|
||||
return datasets.filter((dataset) => dataset.type === 'filesystem');
|
||||
});
|
||||
|
||||
let volumes: Dataset[] = $derived.by(() => {
|
||||
return datasets.filter((dataset) => dataset.type === 'volume');
|
||||
});
|
||||
|
||||
let snapshots: Dataset[] = $derived.by(() => {
|
||||
return datasets.filter((dataset) => dataset.type === 'snapshot');
|
||||
});
|
||||
|
||||
const counts = $derived({
|
||||
pools: pools.length,
|
||||
datasets: datasets.length,
|
||||
file_systems: filesystems.length,
|
||||
volumes: volumes.length,
|
||||
snapshots: snapshots.length
|
||||
});
|
||||
|
||||
function getCardDetails(type: string) {
|
||||
switch (type) {
|
||||
case 'pools':
|
||||
return {
|
||||
title: 'Pools',
|
||||
icon: 'bi--hdd-stack-fill'
|
||||
};
|
||||
case 'datasets':
|
||||
return {
|
||||
title: 'Datasets',
|
||||
icon: 'material-symbols--dataset'
|
||||
};
|
||||
case 'file_systems':
|
||||
return {
|
||||
title: 'Filesystems',
|
||||
icon: 'eos-icons--file-system'
|
||||
};
|
||||
case 'volumes':
|
||||
return {
|
||||
title: 'Volumes',
|
||||
icon: 'carbon--volume-block-storage'
|
||||
};
|
||||
case 'snapshots':
|
||||
return {
|
||||
title: 'Snapshots',
|
||||
icon: 'carbon--ibm-cloud-vpc-block-storage-snapshots'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: '',
|
||||
icon: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let comboBoxes = $state({
|
||||
poolUsage: {
|
||||
open: false,
|
||||
value: '',
|
||||
data: [] as { value: string; label: string }[]
|
||||
},
|
||||
datasetCompression: {
|
||||
open: false,
|
||||
value: '',
|
||||
data: [] as { value: string; label: string }[]
|
||||
},
|
||||
poolStats: {
|
||||
interval: {
|
||||
open: false,
|
||||
value: '1',
|
||||
data: [] as { value: string; label: string }[]
|
||||
},
|
||||
statType: {
|
||||
open: false,
|
||||
value: 'allocated',
|
||||
data: [
|
||||
{ value: 'allocated', label: 'Allocated' },
|
||||
{ value: 'free', label: 'Free' },
|
||||
{ value: 'size', label: 'Size' },
|
||||
{ value: 'dedupRatio', label: 'Dedup Ratio' }
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (pools) {
|
||||
comboBoxes.poolUsage.value = pools[0]?.name || '';
|
||||
comboBoxes.poolUsage.data = pools.map((pool) => ({
|
||||
value: pool.name,
|
||||
label: pool.name
|
||||
}));
|
||||
|
||||
comboBoxes.datasetCompression.value = pools[0]?.name || '';
|
||||
comboBoxes.datasetCompression.data = pools.map((pool) => ({
|
||||
value: pool.name,
|
||||
label: pool.name
|
||||
}));
|
||||
}
|
||||
|
||||
if (poolStats?.intervalMap) {
|
||||
comboBoxes.poolStats.interval.data = poolStats.intervalMap;
|
||||
}
|
||||
|
||||
if (poolStatsInterval) {
|
||||
comboBoxes.poolStats.interval.value = poolStatsInterval;
|
||||
}
|
||||
});
|
||||
|
||||
let pieCharts = $derived.by(() => {
|
||||
return {
|
||||
poolUsage: {
|
||||
data: getPoolUsagePieData(pools, comboBoxes.poolUsage.value)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
let histograms = $derived.by(() => {
|
||||
return {
|
||||
compression: {
|
||||
data: getDatasetCompressionHist(comboBoxes.datasetCompression.value, datasets)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
let { poolStatSeries } = $derived.by(() => {
|
||||
const statType = comboBoxes.poolStats.statType.value;
|
||||
const { poolStatsData, poolStatsKeys } = getPoolStatsCombined(
|
||||
poolStats.poolStatPoint,
|
||||
statType as StatType
|
||||
);
|
||||
|
||||
const poolStatSeries = poolStatsKeys.map((series, index) => ({
|
||||
field: series.key,
|
||||
label: series.title,
|
||||
color: series.color,
|
||||
data: poolStatsData[index].map((item) => ({
|
||||
date: item.date,
|
||||
value: item[series.key]
|
||||
}))
|
||||
}));
|
||||
|
||||
return { poolStatSeries };
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet card(type: string)}
|
||||
<Card.Root class="gap-2 p-5">
|
||||
<Card.Header class="p-0">
|
||||
<Card.Title class="p-0">
|
||||
<div class="flex items-center">
|
||||
<span
|
||||
class={`icon-[${getCardDetails(type).icon}] mr-2`}
|
||||
style="width: 18px; height: 18px;"
|
||||
></span>
|
||||
|
||||
<span class="font-normal">{getCardDetails(type).title}</span>
|
||||
</div>
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content class="p-0 pl-1">
|
||||
<p class="text-xl font-semibold">
|
||||
{counts[type as CardType]}
|
||||
</p>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{/snippet}
|
||||
|
||||
<div class="p-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 lg:grid-cols-5">
|
||||
{#each ['pools', 'datasets', 'file_systems', 'volumes', 'snapshots'] as type}
|
||||
<div>
|
||||
{@render card(type)}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if pools.length > 0}
|
||||
<Card.Root class="mt-4 w-full flex-col">
|
||||
<Card.Header>
|
||||
<Card.Title class="mb-[-100px]">
|
||||
<div
|
||||
class="flex w-full flex-col items-start justify-between gap-2 md:flex-row md:items-center"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--data-usage] mr-2"></span>
|
||||
|
||||
<span class="text-sm font-bold md:text-lg xl:text-xl">Pool Stats</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<CustomComboBox
|
||||
bind:open={comboBoxes.poolStats.statType.open}
|
||||
label=""
|
||||
bind:value={comboBoxes.poolStats.statType.value}
|
||||
data={comboBoxes.poolStats.statType.data}
|
||||
classes=""
|
||||
placeholder="Select a stat type"
|
||||
width="w-48"
|
||||
disallowEmpty={true}
|
||||
/>
|
||||
<CustomComboBox
|
||||
bind:open={comboBoxes.poolStats.interval.open}
|
||||
label=""
|
||||
bind:value={poolStatsInterval}
|
||||
data={comboBoxes.poolStats.interval.data}
|
||||
classes=""
|
||||
placeholder="Select a interval"
|
||||
width="w-48"
|
||||
disallowEmpty={true}
|
||||
/>
|
||||
<Button
|
||||
onclick={() => poolStatsRef?.resetZoom()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-9"
|
||||
>
|
||||
<span class="icon-[carbon--reset] h-4 w-4"></span>
|
||||
|
||||
Reset zoom
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="mt-10 flex-1 overflow-hidden md:mt-2">
|
||||
<AreaChart
|
||||
bind:chart={poolStatsRef}
|
||||
elements={poolStatSeries}
|
||||
formatSize={comboBoxes.poolStats.statType.value !== 'dedupRatio'}
|
||||
containerClass="border-none shadow-none !p-0"
|
||||
icon=""
|
||||
showResetButton={false}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="mt-4 grid h-full w-full grid-cols-12 gap-4">
|
||||
<Card.Root class="col-span-12 min-h-[300px] w-full flex-col md:col-span-8">
|
||||
<Card.Header>
|
||||
<Card.Title class="mb-[-100px]">
|
||||
<div
|
||||
class="flex w-full flex-col items-start justify-between gap-2 md:flex-row md:items-center"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--data-usage] mr-2"></span>
|
||||
|
||||
<span class="text-sm font-bold md:text-lg xl:text-xl">Dataset Compression</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<CustomComboBox
|
||||
bind:open={comboBoxes.datasetCompression.open}
|
||||
label=""
|
||||
bind:value={comboBoxes.datasetCompression.value}
|
||||
data={comboBoxes.datasetCompression.data}
|
||||
classes=""
|
||||
placeholder="Select a pool"
|
||||
width="w-48"
|
||||
disallowEmpty={true}
|
||||
/>
|
||||
<Button
|
||||
onclick={() => datasetChartRef?.resetZoom()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-9"
|
||||
>
|
||||
<span class="icon-[carbon--reset] h-4 w-4"></span>
|
||||
|
||||
Reset zoom
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="mt-10 flex-1 overflow-hidden md:mt-2">
|
||||
{#if histograms.compression.data.length === 0}
|
||||
<div class="flex h-full items-center justify-center">
|
||||
<p class="text-md">No data available</p>
|
||||
</div>
|
||||
{:else}
|
||||
<BarChart
|
||||
bind:chart={datasetChartRef}
|
||||
data={histograms.compression.data}
|
||||
colors={{
|
||||
baseline: 'chart-3',
|
||||
value: 'chart-4'
|
||||
}}
|
||||
formatter="size-formatter"
|
||||
showResetButton={false}
|
||||
/>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root class="col-span-12 flex w-full flex-col md:col-span-4">
|
||||
<Card.Header>
|
||||
<Card.Title class="mb-[-100px]">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--data-usage] mr-2"></span>
|
||||
|
||||
<span class="text-sm font-bold md:text-lg xl:text-xl">Pool Usage</span>
|
||||
</div>
|
||||
<CustomComboBox
|
||||
bind:open={comboBoxes.poolUsage.open}
|
||||
label=""
|
||||
bind:value={comboBoxes.poolUsage.value}
|
||||
data={comboBoxes.poolUsage.data}
|
||||
classes=""
|
||||
placeholder="Select a pool"
|
||||
width="w-48"
|
||||
disallowEmpty={true}
|
||||
/>
|
||||
</div>
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="flex-1 overflow-hidden">
|
||||
<div class="mt-4 flex h-full items-center justify-center">
|
||||
<PieChart
|
||||
containerClass="h-full w-full rounded flex items-start justify-center"
|
||||
data={pieCharts.poolUsage.data}
|
||||
formatter={'size-formatter'}
|
||||
/>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { getDatasets } from '$lib/api/zfs/datasets';
|
||||
import { getPools, getPoolStats } from '$lib/api/zfs/pool';
|
||||
import { SEVEN_DAYS } from '$lib/utils';
|
||||
import { cachedFetch } from '$lib/utils/http';
|
||||
|
||||
export async function load() {
|
||||
const cacheDuration = SEVEN_DAYS;
|
||||
const [datasets, pools, poolStats] = await Promise.all([
|
||||
cachedFetch('datasets', async () => await getDatasets(), cacheDuration),
|
||||
cachedFetch('pools', getPools, cacheDuration),
|
||||
cachedFetch('pool-stats', async () => await getPoolStats(1, 128), cacheDuration)
|
||||
]);
|
||||
|
||||
return {
|
||||
pools: pools,
|
||||
datasets: datasets,
|
||||
poolStats: poolStats
|
||||
};
|
||||
}
|
||||
|
||||
@@ -25,12 +25,9 @@
|
||||
import humanFormat from 'human-format';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { storage } from '$lib';
|
||||
import { resource, useInterval, Debounced, IsDocumentVisible } from 'runed';
|
||||
import { untrack } from 'svelte';
|
||||
import { resource, useInterval, Debounced, IsDocumentVisible, watch } from 'runed';
|
||||
import type { GFSStep } from '$lib/types/common';
|
||||
import SimpleSelect from '$lib/components/custom/SimpleSelect.svelte';
|
||||
import LayerBrush from '$lib/components/custom/Charts/LayerBrush.svelte';
|
||||
import EChart from '$lib/components/custom/Charts/EChartSample.svelte';
|
||||
import LineBrush from '$lib/components/custom/Charts/LineBrush/Single.svelte';
|
||||
|
||||
interface Data {
|
||||
@@ -50,7 +47,7 @@
|
||||
updateCache(key, result);
|
||||
return result;
|
||||
},
|
||||
{ lazy: true, initialValue: data.vm }
|
||||
{ initialValue: data.vm }
|
||||
);
|
||||
|
||||
const domain = resource(
|
||||
@@ -60,7 +57,7 @@
|
||||
updateCache(key, result);
|
||||
return result;
|
||||
},
|
||||
{ lazy: true, initialValue: data.domain }
|
||||
{ initialValue: data.domain }
|
||||
);
|
||||
|
||||
const stats = resource(
|
||||
@@ -71,7 +68,7 @@
|
||||
updateCache(key, result);
|
||||
return result;
|
||||
},
|
||||
{ lazy: true, initialValue: data.stats }
|
||||
{ initialValue: data.stats }
|
||||
);
|
||||
|
||||
const visible = new IsDocumentVisible();
|
||||
@@ -86,15 +83,16 @@
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (storage.visible) {
|
||||
untrack(() => {
|
||||
watch(
|
||||
() => storage.idle,
|
||||
(idle) => {
|
||||
if (!idle) {
|
||||
vm.refetch();
|
||||
domain.refetch();
|
||||
stats.refetch();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
let recentStat = $derived(
|
||||
stats.current[stats.current.length - 1] || getObjectSchemaDefaults(VMStatSchema)
|
||||
@@ -450,6 +448,17 @@
|
||||
color="one"
|
||||
containerContentHeight="h-64"
|
||||
/>
|
||||
|
||||
<LineBrush
|
||||
title="Memory Usage"
|
||||
points={stats.current.map((data) => ({
|
||||
date: new Date(data.createdAt).getTime(),
|
||||
value: Number(data.memoryUsage)
|
||||
}))}
|
||||
percentage={true}
|
||||
color="two"
|
||||
containerContentHeight="h-64"
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,8 @@ export async function load({ params }) {
|
||||
cachedFetch(`vm-stats-${rid}`, async () => getStats(Number(rid), 'hourly'), cacheDuration)
|
||||
]);
|
||||
|
||||
console.log('VM Summary Load:', { rid, vm, domain, stats });
|
||||
|
||||
return {
|
||||
rid: Number(rid),
|
||||
vm: vm,
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
import type { Column, Row } from '$lib/types/components/tree-table';
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { renderWithIcon } from '$lib/utils/table';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { CellComponent } from 'tabulator-tables';
|
||||
import { resource } from 'runed';
|
||||
|
||||
interface Data {
|
||||
cluster: ClusterDetails;
|
||||
@@ -23,34 +23,33 @@
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
let reload = $state(false);
|
||||
const results = createQuery(() => ({
|
||||
queryKey: ['cluster-info'],
|
||||
queryFn: async () => {
|
||||
return await getDetails();
|
||||
|
||||
const datacenter = resource(
|
||||
() => 'cluster-info',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const res = await getDetails();
|
||||
updateCache('cluster-info', res);
|
||||
return res;
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.cluster,
|
||||
refetchOnMount: 'always',
|
||||
onSuccess: (data: ClusterDetails) => {
|
||||
updateCache('cluster-info', data);
|
||||
}
|
||||
}));
|
||||
{ initialValue: data.cluster }
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.refetch();
|
||||
datacenter.refetch();
|
||||
reload = false;
|
||||
}
|
||||
});
|
||||
|
||||
let dataCenter = $derived(results.data);
|
||||
let canReset = $derived(dataCenter?.cluster.enabled === true);
|
||||
let canReset = $derived(datacenter.current.cluster.enabled === true);
|
||||
let canCreate = $derived(
|
||||
dataCenter?.cluster.raftBootstrap === null && dataCenter?.cluster.enabled === false
|
||||
datacenter.current.cluster.raftBootstrap === null &&
|
||||
datacenter.current.cluster.enabled === false
|
||||
);
|
||||
|
||||
let canJoin = $derived(
|
||||
dataCenter?.cluster.raftBootstrap !== true && dataCenter?.cluster.enabled === false
|
||||
datacenter.current.cluster.raftBootstrap !== true &&
|
||||
datacenter.current.cluster.enabled === false
|
||||
);
|
||||
|
||||
let modals = $state({
|
||||
@@ -117,8 +116,8 @@
|
||||
}
|
||||
];
|
||||
|
||||
if (dataCenter?.nodes) {
|
||||
for (const node of dataCenter.nodes) {
|
||||
if (datacenter.current.nodes) {
|
||||
for (const node of datacenter.current.nodes) {
|
||||
rows.push({
|
||||
id: node.id,
|
||||
leader: node.isLeader,
|
||||
@@ -199,7 +198,7 @@
|
||||
</div>
|
||||
|
||||
<Create bind:open={modals.create.open} bind:reload />
|
||||
<JoinInformation bind:open={modals.view.open} cluster={dataCenter} />
|
||||
<JoinInformation bind:open={modals.view.open} cluster={datacenter.current} />
|
||||
<Join bind:open={modals.join.open} bind:reload />
|
||||
|
||||
<AlertDialog
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
import { handleAPIError, isAPIResponse, updateCache } from '$lib/utils/http';
|
||||
import { generateTableData, markdownToTailwindHTML } from '$lib/utils/info/notes';
|
||||
import { convertDbTime } from '$lib/utils/time';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import Icon from '@iconify/svelte';
|
||||
import { resource, watch } from 'runed';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { CellComponent } from 'tabulator-tables';
|
||||
|
||||
@@ -22,19 +23,28 @@
|
||||
}
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
|
||||
const results = createQuery(() => ({
|
||||
queryKey: ['cluster-notes'],
|
||||
queryFn: async () => {
|
||||
let notes = resource(
|
||||
() => 'cluster-notes',
|
||||
async () => {
|
||||
return (await getNotes()) as Note[];
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: true,
|
||||
initialData: data.notes,
|
||||
refetchOnMount: 'always'
|
||||
}));
|
||||
{
|
||||
initialValue: data.notes
|
||||
}
|
||||
);
|
||||
|
||||
let reload = $state(false);
|
||||
|
||||
watch(
|
||||
() => reload,
|
||||
(value) => {
|
||||
if (value) {
|
||||
notes.refetch();
|
||||
reload = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
let notes: Note[] = $derived(results.data as Note[]);
|
||||
let modalState = $state({
|
||||
title: '',
|
||||
content: '',
|
||||
@@ -81,7 +91,7 @@
|
||||
// }
|
||||
} else {
|
||||
const response = await createNote(modalState.title, modalState.content);
|
||||
results.refetch();
|
||||
reload = true;
|
||||
if (response.status === 'success') {
|
||||
toast.success('Note created', { position: 'bottom-center' });
|
||||
handleNote(undefined, false, true);
|
||||
@@ -97,7 +107,7 @@
|
||||
}
|
||||
|
||||
function viewNote(id: number | string | undefined) {
|
||||
const note = notes.find((note) => note.id === id);
|
||||
const note = notes.current.find((note) => note.id === id);
|
||||
if (note) {
|
||||
modalState.title = note.title;
|
||||
modalState.content = note.content;
|
||||
@@ -107,7 +117,7 @@
|
||||
}
|
||||
|
||||
function handleDelete(id: number | string | undefined) {
|
||||
const note = notes.find((note) => note.id === id);
|
||||
const note = notes.current.find((note) => note.id === id);
|
||||
if (note) {
|
||||
modalState.title = note.title;
|
||||
modalState.content = note.content;
|
||||
@@ -117,7 +127,7 @@
|
||||
}
|
||||
|
||||
function handleBulkDelete(ids: number[]) {
|
||||
const notesToDelete = notes.filter((note) => ids.includes(note.id));
|
||||
const notesToDelete = notes.current.filter((note) => ids.includes(note.id));
|
||||
if (notesToDelete.length > 0) {
|
||||
modalState.title = `${notesToDelete.length} notes`;
|
||||
modalState.isBulkDeleteOpen = true;
|
||||
@@ -153,7 +163,7 @@
|
||||
}
|
||||
]);
|
||||
|
||||
let tableData = $derived(generateTableData(columns, notes));
|
||||
let tableData = $derived(generateTableData(columns, notes.current));
|
||||
let activeRow: Row[] | null = $state(null);
|
||||
let query: string = $state('');
|
||||
</script>
|
||||
@@ -168,8 +178,7 @@
|
||||
class="h-6.5"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--eye] mr-1 h-4 w-4"></span>
|
||||
|
||||
<Icon icon="mdi:eye" class="mr-1 h-4 w-4" />
|
||||
<span>View</span>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -183,7 +192,7 @@
|
||||
class="h-6.5"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--delete] mr-1 h-4 w-4"></span>
|
||||
<Icon icon="mdi:delete" class="mr-1 h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -192,7 +201,7 @@
|
||||
{#if type === 'edit-note'}
|
||||
<Button
|
||||
onclick={() => {
|
||||
const note = notes.find((note) => activeRow && note.id === activeRow[0]?.id);
|
||||
const note = notes.current.find((note) => activeRow && note.id === activeRow[0]?.id);
|
||||
handleNote(note, true);
|
||||
}}
|
||||
size="sm"
|
||||
@@ -200,7 +209,7 @@
|
||||
class="h-6.5"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--note-edit] mr-1 h-4 w-4"></span>
|
||||
<Icon icon="mdi:note-edit" class="mr-1 h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -219,7 +228,7 @@
|
||||
class="h-6.5"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[material-symbols--delete-sweep] mr-1 h-4 w-4"></span>
|
||||
<Icon icon="material-symbols:delete-sweep" class="mr-1 h-4 w-4" />
|
||||
<span>Bulk Delete</span>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -233,7 +242,7 @@
|
||||
|
||||
<Button onclick={() => handleNote()} size="sm" class="h-6 ">
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[gg--add] mr-1 h-4 w-4"></span>
|
||||
<Icon icon="gg:add" class="mr-1 h-4 w-4" />
|
||||
<span>New</span>
|
||||
</div>
|
||||
</Button>
|
||||
@@ -249,8 +258,7 @@
|
||||
<Dialog.Header class="">
|
||||
<Dialog.Title class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class={`icon-[${modalState.isEditMode ? 'mdi:note-edit' : 'mdi:note'}] h-5 w-5`}
|
||||
></span>
|
||||
<Icon icon={modalState.isEditMode ? 'mdi:note-edit' : 'mdi:note'} class="h-5 w-5" />
|
||||
<span>
|
||||
{#if modalState.isEditMode}
|
||||
{#if selectedId}
|
||||
@@ -276,8 +284,7 @@
|
||||
modalState.content = '';
|
||||
}}
|
||||
>
|
||||
<span class="icon-[radix-icons--reset] pointer-events-none h-4 w-4"></span>
|
||||
|
||||
<Icon icon="radix-icons:reset" class="pointer-events-none h-4 w-4" />
|
||||
<span class="sr-only">{'Reset'}</span>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -291,8 +298,7 @@
|
||||
modalState.content = '';
|
||||
}}
|
||||
>
|
||||
<span class="icon-[material-symbols--close-rounded] pointer-events-none h-4 w-4"
|
||||
></span>
|
||||
<Icon icon="material-symbols:close-rounded" class="pointer-events-none h-4 w-4" />
|
||||
<span class="sr-only">{'Close'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -354,8 +360,7 @@
|
||||
onConfirm: async () => {
|
||||
const id = activeRow ? activeRow[0]?.id : null;
|
||||
const result = await deleteNote(id as number);
|
||||
|
||||
results.refetch();
|
||||
reload = true;
|
||||
if (isAPIResponse(result) && result.status === 'success') {
|
||||
toast.success('Note deleted', { position: 'bottom-center' });
|
||||
handleNote(undefined, false, true);
|
||||
@@ -381,7 +386,7 @@
|
||||
? activeRow.map((row) => (typeof row.id === 'number' ? row.id : parseInt(row.id)))
|
||||
: [];
|
||||
const result = await deleteNote(ids[0]);
|
||||
results.refetch();
|
||||
reload = true;
|
||||
if (isAPIResponse(result) && result.status === 'success') {
|
||||
toast.success('Notes deleted', { position: 'bottom-center' });
|
||||
handleNote(undefined, false, true);
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { deleteS3Storage, getStorages } from '$lib/api/cluster/storage';
|
||||
import Create from '$lib/components/custom/Cluster/Storage/Create.svelte';
|
||||
import AlertDialog from '$lib/components/custom/Dialog/Alert.svelte';
|
||||
import TreeTable from '$lib/components/custom/TreeTable.svelte';
|
||||
import Search from '$lib/components/custom/TreeTable/Search.svelte';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import type { ClusterStorages } from '$lib/types/cluster/storage';
|
||||
import type { Column, Row } from '$lib/types/components/tree-table';
|
||||
import { handleAPIError, updateCache } from '$lib/utils/http';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
interface Data {
|
||||
storages: ClusterStorages;
|
||||
}
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
const results = createQuery(() => ({
|
||||
queryKey: ['cluster-storages'],
|
||||
queryFn: async () => {
|
||||
return await getStorages();
|
||||
},
|
||||
keepPreviousData: true,
|
||||
initialData: data.storages,
|
||||
refetchOnMount: 'always',
|
||||
onSuccess: (data: ClusterStorages) => {
|
||||
updateCache('cluster-storages', data);
|
||||
}
|
||||
}));
|
||||
|
||||
let reload = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reload) {
|
||||
results.refetch();
|
||||
activeRows = null;
|
||||
reload = false;
|
||||
}
|
||||
});
|
||||
|
||||
let storages = $derived(results.data as ClusterStorages);
|
||||
|
||||
let table = $derived.by(() => {
|
||||
const rows = [];
|
||||
const columns: Column[] = [
|
||||
{
|
||||
field: 'id',
|
||||
title: 'ID',
|
||||
visible: false
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: 'Type'
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
title: 'Name'
|
||||
},
|
||||
{
|
||||
field: 'details',
|
||||
title: 'Details',
|
||||
formatter: 'html'
|
||||
}
|
||||
];
|
||||
|
||||
for (const s3Storage of storages.s3) {
|
||||
rows.push({
|
||||
id: s3Storage.id,
|
||||
type: 'S3',
|
||||
name: s3Storage.name,
|
||||
details: `Bucket: ${s3Storage.bucket}<br/>Region: ${s3Storage.region}`
|
||||
});
|
||||
}
|
||||
|
||||
for (const dirStorage of storages.directories) {
|
||||
rows.push({
|
||||
id: dirStorage.id,
|
||||
type: 'Directory',
|
||||
name: dirStorage.name,
|
||||
details: `Path: ${dirStorage.path}`
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
columns,
|
||||
rows
|
||||
};
|
||||
});
|
||||
|
||||
let activeRows: Row[] | null = $state(null);
|
||||
let activeRow: Row | null = $derived(activeRows ? (activeRows[0] as Row) : ({} as Row));
|
||||
|
||||
let query = $state('');
|
||||
let modals = $state({
|
||||
create: {
|
||||
open: false
|
||||
},
|
||||
delete: {
|
||||
open: false,
|
||||
type: '' as '' | 's3'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet button(type: string)}
|
||||
{#if activeRows && activeRows?.length !== 0}
|
||||
{#if type === 'delete'}
|
||||
<Button
|
||||
onclick={() => {
|
||||
modals.delete.open = true;
|
||||
if (activeRow?.type === 'S3') {
|
||||
modals.delete.type = 's3';
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-6.5"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[mdi--delete] mr-1 h-4 w-4"></span>
|
||||
|
||||
<span>Delete</span>
|
||||
</div>
|
||||
</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<div class="flex h-10 w-full items-center gap-2 border-b p-2">
|
||||
<Search bind:query />
|
||||
|
||||
<Button onclick={() => (modals.create.open = true)} size="sm" class="h-6 ">
|
||||
<div class="flex items-center">
|
||||
<span class="icon-[gg--add] mr-1 h-4 w-4"></span>
|
||||
<span>New</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
{@render button('delete')}
|
||||
</div>
|
||||
|
||||
<TreeTable
|
||||
name="cluster-storages-tt"
|
||||
data={table}
|
||||
{query}
|
||||
bind:parentActiveRow={activeRows}
|
||||
multipleSelect={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if modals.create.open}
|
||||
<Create bind:open={modals.create.open} bind:reload {storages} />
|
||||
{/if}
|
||||
|
||||
<AlertDialog
|
||||
open={modals.delete.open}
|
||||
customTitle={`This will delete ${activeRow?.name}`}
|
||||
actions={{
|
||||
onConfirm: async () => {
|
||||
let deleteFunc = modals.delete.type === 's3' ? deleteS3Storage : deleteS3Storage;
|
||||
|
||||
const response = await deleteFunc(Number(activeRow?.id));
|
||||
reload = true;
|
||||
if (response.error) {
|
||||
handleAPIError(response);
|
||||
toast.error(`Failed to delete ${activeRow?.name}`, {
|
||||
position: 'bottom-center'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(`Deleted ${activeRow?.name}`, {
|
||||
position: 'bottom-center'
|
||||
});
|
||||
|
||||
modals.delete.open = false;
|
||||
},
|
||||
onCancel: () => {
|
||||
modals.delete.open = false;
|
||||
}
|
||||
}}
|
||||
></AlertDialog>
|
||||
@@ -1,16 +0,0 @@
|
||||
import { getNotes } from '$lib/api/cluster/notes';
|
||||
import { getStorages } from '$lib/api/cluster/storage';
|
||||
import type { Note } from '$lib/types/info/notes';
|
||||
import { SEVEN_DAYS } from '$lib/utils';
|
||||
import { cachedFetch } from '$lib/utils/http';
|
||||
|
||||
export async function load() {
|
||||
const cacheDuration = SEVEN_DAYS;
|
||||
const [storages] = await Promise.all([
|
||||
cachedFetch('cluster-storages', async () => getStorages(), cacheDuration)
|
||||
]);
|
||||
|
||||
return {
|
||||
storages: storages
|
||||
};
|
||||
}
|
||||
@@ -2,21 +2,21 @@
|
||||
import { getDetails, getNodes } from '$lib/api/cluster/cluster';
|
||||
import { getCPUInfo } from '$lib/api/info/cpu';
|
||||
import { getRAMInfo } from '$lib/api/info/ram';
|
||||
import { getPoolsDiskUsage, getPoolsDiskUsageFull } from '$lib/api/zfs/pool';
|
||||
import { getPoolsDiskUsageFull } from '$lib/api/zfs/pool';
|
||||
import Arc from '$lib/components/custom/Charts/Arc.svelte';
|
||||
import { Badge } from '$lib/components/ui/badge/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import type { ClusterDetails, ClusterNode } from '$lib/types/cluster/cluster';
|
||||
import type { CPUInfo, CPUInfoHistorical } from '$lib/types/info/cpu';
|
||||
import type { RAMInfo, RAMInfoHistorical } from '$lib/types/info/ram';
|
||||
import type { CPUInfo } from '$lib/types/info/cpu';
|
||||
import type { RAMInfo } from '$lib/types/info/ram';
|
||||
import type { PoolsDiskUsage } from '$lib/types/zfs/pool';
|
||||
import { getQuorumStatus } from '$lib/utils/cluster';
|
||||
import { updateCache } from '$lib/utils/http';
|
||||
import { capitalizeFirstLetter } from '$lib/utils/string';
|
||||
import { dateToAgo } from '$lib/utils/time';
|
||||
import { createQueries } from '@tanstack/svelte-query';
|
||||
import humanFormat from 'human-format';
|
||||
import { resource } from 'runed';
|
||||
|
||||
interface Data {
|
||||
nodes: ClusterNode[];
|
||||
@@ -27,78 +27,73 @@
|
||||
}
|
||||
|
||||
let { data }: { data: Data } = $props();
|
||||
const results = createQueries(() => ({
|
||||
queries: [
|
||||
{
|
||||
queryKey: ['cluster-nodes'],
|
||||
queryFn: async () => {
|
||||
return (await getNodes()) as ClusterNode[];
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: true,
|
||||
initialData: data.nodes,
|
||||
refetchOnMount: 'always',
|
||||
onSuccess: (data: ClusterNode[]) => {
|
||||
updateCache('cluster-nodes', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['cluster-details'],
|
||||
queryFn: async () => {
|
||||
return (await getDetails()) as ClusterDetails;
|
||||
},
|
||||
refetchInterval: 1000,
|
||||
keepPreviousData: true,
|
||||
initialData: data.details,
|
||||
refetchOnMount: 'always',
|
||||
onSuccess: (data: ClusterDetails) => {
|
||||
updateCache('cluster-details', data);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['cpu-info'],
|
||||
queryFn: getCPUInfo,
|
||||
keepPreviousData: true,
|
||||
initialData: data.cpu,
|
||||
refetchOnMount: 'always',
|
||||
onSuccess: (data: CPUInfo | CPUInfoHistorical) => {
|
||||
updateCache('cpu-info', data as CPUInfo);
|
||||
}
|
||||
},
|
||||
{
|
||||
queryKey: ['ram-info'],
|
||||
queryFn: getRAMInfo,
|
||||
keepPreviousData: true,
|
||||
initialData: data.ram,
|
||||
onSuccess: (data: RAMInfo | RAMInfoHistorical) => {
|
||||
updateCache('ram-info', data);
|
||||
},
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true
|
||||
},
|
||||
{
|
||||
queryKey: ['total-disk-usage'],
|
||||
queryFn: getPoolsDiskUsageFull,
|
||||
keepPreviousData: true,
|
||||
initialData: data.disk,
|
||||
onSuccess: (data: PoolsDiskUsage) => {
|
||||
updateCache('total-disk-usage', data);
|
||||
},
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
let nodes = $derived(results[0].data ?? []);
|
||||
let clusterDetails = $derived(results[1].data);
|
||||
let cpuInfo = $derived(results[2].data as CPUInfo);
|
||||
let ramInfo = $derived(results[3].data as RAMInfo);
|
||||
let diskInfo = $derived(results[4].data as PoolsDiskUsage);
|
||||
let clustered = $derived(clusterDetails?.cluster.enabled || false);
|
||||
let nodes = resource(
|
||||
() => 'cluster-nodes',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const result = await getNodes();
|
||||
updateCache('cluster-nodes', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.nodes
|
||||
}
|
||||
);
|
||||
|
||||
let clusterDetails = resource(
|
||||
() => 'cluster-details',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const result = await getDetails();
|
||||
updateCache('cluster-details', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.details
|
||||
}
|
||||
);
|
||||
|
||||
let cpuInfo = resource(
|
||||
() => 'cpu-info',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const result = await getCPUInfo('current');
|
||||
updateCache('cpu-info', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.cpu
|
||||
}
|
||||
);
|
||||
|
||||
let ramInfo = resource(
|
||||
() => 'ram-info',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const result = await getRAMInfo('current');
|
||||
updateCache('ram-info', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.ram
|
||||
}
|
||||
);
|
||||
|
||||
let diskInfo = resource(
|
||||
() => 'total-disk-usage',
|
||||
async (key, prevKey, { signal }) => {
|
||||
const result = await getPoolsDiskUsageFull();
|
||||
updateCache('total-disk-usage', result);
|
||||
return result;
|
||||
},
|
||||
{
|
||||
initialValue: data.disk
|
||||
}
|
||||
);
|
||||
|
||||
let clustered = $derived.by(() => {
|
||||
return clusterDetails?.current.cluster.enabled ?? false;
|
||||
});
|
||||
|
||||
let total = $derived.by(() => {
|
||||
if (nodes.length === 0) {
|
||||
if (nodes.current.length === 0) {
|
||||
return {
|
||||
cpu: { total: 0, usage: 0 },
|
||||
ram: { total: 0, usage: 0 },
|
||||
@@ -106,17 +101,20 @@
|
||||
};
|
||||
}
|
||||
|
||||
const totalCPUs = nodes.reduce((acc, node) => acc + node.cpu, 0);
|
||||
const used = nodes.reduce((acc, node) => acc + (node.cpu * node.cpuUsage) / 100, 0);
|
||||
const totalCPUs = nodes.current.reduce((acc, node) => acc + node.cpu, 0);
|
||||
const used = nodes.current.reduce((acc, node) => acc + (node.cpu * node.cpuUsage) / 100, 0);
|
||||
|
||||
const totalMemory = nodes.reduce((acc, node) => acc + node.memory, 0);
|
||||
const usedMemory = nodes.reduce(
|
||||
const totalMemory = nodes.current.reduce((acc, node) => acc + node.memory, 0);
|
||||
const usedMemory = nodes.current.reduce(
|
||||
(acc, node) => acc + ((node.memory ?? 0) * (node.memoryUsage ?? 0)) / 100,
|
||||
0
|
||||
);
|
||||
|
||||
const totalDisk = nodes.reduce((acc, node) => acc + node.disk, 0);
|
||||
const usedDisk = nodes.reduce((acc, node) => acc + (node.disk * node.diskUsage) / 100, 0);
|
||||
const totalDisk = nodes.current.reduce((acc, node) => acc + node.disk, 0);
|
||||
const usedDisk = nodes.current.reduce(
|
||||
(acc, node) => acc + (node.disk * node.diskUsage) / 100,
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
cpu: {
|
||||
@@ -134,9 +132,9 @@
|
||||
};
|
||||
});
|
||||
|
||||
let quorumStatus = $derived(getQuorumStatus(clusterDetails as ClusterDetails, nodes));
|
||||
let quorumStatus = $derived(getQuorumStatus(clusterDetails.current, nodes.current));
|
||||
let statusCounts = $derived.by(() => {
|
||||
return nodes.reduce(
|
||||
return nodes.current.reduce(
|
||||
(acc, node) => {
|
||||
acc[node.status] = (acc[node.status] || 0) + 1;
|
||||
return acc;
|
||||
@@ -227,21 +225,25 @@
|
||||
<Arc value={total.disk.usage} subtitle={humanFormat(total.disk.total)} title="Disk" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-1 justify-center">
|
||||
<Arc value={cpuInfo?.usage} title="CPU" subtitle="{cpuInfo?.physicalCores} vCPUs" />
|
||||
</div>
|
||||
<div class="flex flex-1 justify-center">
|
||||
<Arc
|
||||
value={ramInfo?.usedPercent}
|
||||
title="RAM"
|
||||
subtitle={humanFormat(ramInfo?.total || 0)}
|
||||
value={cpuInfo.current.usage}
|
||||
title="CPU"
|
||||
subtitle="{cpuInfo.current.physicalCores} vCPUs"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 justify-center">
|
||||
<Arc
|
||||
value={diskInfo?.usage || 0}
|
||||
value={ramInfo.current.usedPercent}
|
||||
title="RAM"
|
||||
subtitle={humanFormat(ramInfo.current.total || 0)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 justify-center">
|
||||
<Arc
|
||||
value={diskInfo.current.usage || 0}
|
||||
title="Disk"
|
||||
subtitle={humanFormat(diskInfo?.total || 0)}
|
||||
subtitle={humanFormat(diskInfo.current.total || 0)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -274,7 +276,7 @@
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each nodes as node (node.id)}
|
||||
{#each nodes.current as node (node.id)}
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
<Badge variant="outline" class="text-muted-foreground px-1.5">
|
||||
|
||||
Reference in New Issue
Block a user