mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-10 08:00:32 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6565ff6077 |
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||
total, list, err := containerService.Page(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -249,10 +249,9 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
||||
// @Success 200 {object} dto.ContainerStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param containersOnly query boolean false "Only count containers"
|
||||
// @Router /containers/status [get]
|
||||
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
||||
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
|
||||
data, err := containerService.LoadStatus()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -416,14 +415,9 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
||||
// @Success 200 {array} dto.ContainerListStats
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||
// @Router /containers/list/stats [get]
|
||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
||||
var ids []string
|
||||
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
|
||||
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
|
||||
}
|
||||
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
|
||||
data, err := containerService.ContainerListStats()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -970,12 +964,3 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
|
||||
|
||||
containerService.StreamLogs(c, streamLog)
|
||||
}
|
||||
|
||||
func (b *BaseApi) CleanNetworks(c *gin.Context) {
|
||||
result, err := containerService.CleanNetworks()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /runtimes/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||
var req request.RuntimeOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -83,7 +83,6 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.CreateRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -108,7 +107,6 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.EditRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package dto
|
||||
|
||||
type NetworkCleanupReport struct {
|
||||
Deleted []NetworkCleanupItem `json:"deleted"`
|
||||
Skipped []NetworkCleanupItem `json:"skipped"`
|
||||
Failed []NetworkCleanupItem `json:"failed"`
|
||||
}
|
||||
|
||||
type NetworkCleanupItem struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type NetworkCleanupTask struct {
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
EncryptionMode string `json:"encryptionMode"`
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
|
||||
@@ -58,10 +58,10 @@ type ContainerService struct{}
|
||||
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
|
||||
|
||||
type IContainerService interface {
|
||||
Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error)
|
||||
Page(req dto.PageContainer) (int64, interface{}, error)
|
||||
List() []dto.ContainerOptions
|
||||
ListByImage(imageName string) []dto.ContainerOptions
|
||||
LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error)
|
||||
LoadStatus() (dto.ContainerStatus, error)
|
||||
PageNetwork(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
ListNetwork() ([]dto.Options, error)
|
||||
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
||||
@@ -80,7 +80,7 @@ type IContainerService interface {
|
||||
ContainerUpdate(req dto.ContainerOperate) error
|
||||
ContainerUpgrade(req dto.ContainerUpgrade) error
|
||||
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
||||
ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error)
|
||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
||||
ContainerItemStats(ctx context.Context, req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||
ContainerRename(req dto.ContainerRename) error
|
||||
@@ -92,7 +92,6 @@ type IContainerService interface {
|
||||
|
||||
Inspect(req dto.InspectReq) (string, error)
|
||||
DeleteNetwork(req dto.BatchDelete) error
|
||||
CleanNetworks() (*dto.NetworkCleanupTask, error)
|
||||
CreateNetwork(req dto.NetworkCreate) error
|
||||
DeleteVolume(req dto.BatchDelete) error
|
||||
CreateVolume(req dto.VolumeCreate) error
|
||||
@@ -113,7 +112,7 @@ func NewIContainerService() IContainerService {
|
||||
return &ContainerService{}
|
||||
}
|
||||
|
||||
func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int64, interface{}, error) {
|
||||
func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
@@ -124,7 +123,7 @@ func (u *ContainerService) Page(ctx context.Context, req dto.PageContainer) (int
|
||||
options.Filters = filters.NewArgs()
|
||||
options.Filters.Add("label", req.Filters)
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, options)
|
||||
containers, err := client.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
@@ -207,30 +206,27 @@ func (u *ContainerService) ListByImage(imageName string) []dto.ContainerOptions
|
||||
return options
|
||||
}
|
||||
|
||||
func (u *ContainerService) LoadStatus(ctx context.Context, containersOnly bool) (dto.ContainerStatus, error) {
|
||||
func (u *ContainerService) LoadStatus() (dto.ContainerStatus, error) {
|
||||
var data dto.ContainerStatus
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
defer client.Close()
|
||||
if !containersOnly {
|
||||
images, _ := client.ImageList(ctx, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(ctx, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(ctx, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(ctx, client)
|
||||
}
|
||||
containers, err := client.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return data, err
|
||||
}
|
||||
c := context.Background()
|
||||
|
||||
images, _ := client.ImageList(c, image.ListOptions{All: true})
|
||||
data.ImageCount = len(images)
|
||||
repo, _ := imageRepoRepo.List()
|
||||
data.RepoCount = len(repo)
|
||||
templates, _ := composeRepo.List()
|
||||
data.ComposeTemplateCount = len(templates)
|
||||
networks, _ := client.NetworkList(c, network.ListOptions{})
|
||||
data.NetworkCount = len(networks)
|
||||
volumes, _ := client.VolumeList(c, volume.ListOptions{})
|
||||
data.VolumeCount = len(volumes.Volumes)
|
||||
data.ComposeCount = loadComposeCount(client)
|
||||
containers, _ := client.ContainerList(c, container.ListOptions{All: true})
|
||||
data.ContainerCount = len(containers)
|
||||
for _, item := range containers {
|
||||
switch item.State {
|
||||
@@ -296,65 +292,27 @@ func (u *ContainerService) ContainerItemStats(ctx context.Context, req dto.Opera
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
func (u *ContainerService) ContainerListStats(ctx context.Context, ids []string) ([]dto.ContainerListStats, error) {
|
||||
func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error) {
|
||||
client, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
options := container.ListOptions{All: true}
|
||||
if ids != nil {
|
||||
if len(ids) == 0 {
|
||||
return []dto.ContainerListStats{}, nil
|
||||
}
|
||||
options.Filters = filters.NewArgs()
|
||||
for _, id := range ids {
|
||||
options.Filters.Add("id", id)
|
||||
}
|
||||
}
|
||||
list, err := client.ContainerList(ctx, options)
|
||||
list, err := client.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return collectContainerStats(ctx, list, func(ctx context.Context, id string) dto.ContainerListStats {
|
||||
return loadCpuAndMem(ctx, client, id)
|
||||
}), nil
|
||||
}
|
||||
|
||||
// A fixed worker pool bounds Docker stats requests, including for legacy callers
|
||||
// that request all containers. Stopped containers do not need a stats sample.
|
||||
func collectContainerStats(ctx context.Context, list []container.Summary, load func(context.Context, string) dto.ContainerListStats) []dto.ContainerListStats {
|
||||
datas := make([]dto.ContainerListStats, len(list))
|
||||
for i, item := range list {
|
||||
datas[i].ContainerID = item.ID
|
||||
}
|
||||
jobs := make(chan int)
|
||||
var wg sync.WaitGroup
|
||||
for worker := 0; worker < min(8, len(list)); worker++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for index := range jobs {
|
||||
if ctx.Err() != nil || list[index].State != "running" {
|
||||
continue
|
||||
}
|
||||
sampleCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
datas[index] = load(sampleCtx, list[index].ID)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
wg.Add(len(list))
|
||||
for i := 0; i < len(list); i++ {
|
||||
go func(index int, item container.Summary) {
|
||||
datas[index] = loadCpuAndMem(client, item.ID)
|
||||
wg.Done()
|
||||
}(i, list[i])
|
||||
}
|
||||
dispatch:
|
||||
for index := range list {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break dispatch
|
||||
case jobs <- index:
|
||||
}
|
||||
}
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
return datas
|
||||
return datas, nil
|
||||
}
|
||||
|
||||
func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) {
|
||||
@@ -1757,11 +1715,11 @@ func selectImageRepo(imageName string, repos []model.ImageRepo) *model.ImageRepo
|
||||
return selected
|
||||
}
|
||||
|
||||
func loadCpuAndMem(ctx context.Context, client *client.Client, containerItem string) dto.ContainerListStats {
|
||||
func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerListStats {
|
||||
data := dto.ContainerListStats{
|
||||
ContainerID: containerItem,
|
||||
}
|
||||
res, err := client.ContainerStats(ctx, containerItem, false)
|
||||
res, err := client.ContainerStats(context.Background(), containerItem, false)
|
||||
if err != nil {
|
||||
return data
|
||||
}
|
||||
@@ -1984,11 +1942,11 @@ func transPortToStr(ports []container.Port) []string {
|
||||
return docker.SimplifyPorts(ports)
|
||||
}
|
||||
|
||||
func loadComposeCount(ctx context.Context, client *client.Client) int {
|
||||
func loadComposeCount(client *client.Client) int {
|
||||
options := container.ListOptions{All: true}
|
||||
options.Filters = filters.NewArgs()
|
||||
options.Filters.Add("label", composeProjectLabel)
|
||||
list, err := client.ContainerList(ctx, options)
|
||||
list, err := client.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
)
|
||||
|
||||
var networkCleanupMu sync.Mutex
|
||||
|
||||
func (u *ContainerService) CleanNetworks() (*dto.NetworkCleanupTask, error) {
|
||||
taskItem, err := task.NewTaskWithOps(i18n.GetMsgByKey("Network"), task.TaskClean, task.TaskScopeContainer, "", 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
taskItem.AddSubTask(i18n.GetMsgByKey("TaskClean"), func(t *task.Task) error {
|
||||
networkCleanupMu.Lock()
|
||||
defer networkCleanupMu.Unlock()
|
||||
if err := t.TaskCtx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cli.Close()
|
||||
return executeNetworkCleanup(t, cli)
|
||||
}, nil)
|
||||
go func() {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
global.LOG.Errorf("network cleanup task %s failed: %v", taskItem.TaskID, err)
|
||||
}
|
||||
}()
|
||||
return &dto.NetworkCleanupTask{TaskID: taskItem.TaskID}, nil
|
||||
}
|
||||
|
||||
func executeNetworkCleanup(t *task.Task, cli docker.NetworkCleanupClient) error {
|
||||
t.Log(i18n.GetMsgByKey("PruneStart"))
|
||||
report, err := docker.CleanUnusedNetworks(t.TaskCtx, cli, func(status string, item dto.NetworkCleanupItem) {
|
||||
key := "NetworkCleanupDeleted"
|
||||
if status == "skipped" || status == "failed" {
|
||||
key = map[string]string{
|
||||
"protected": "NetworkCleanupProtected",
|
||||
"container_connected": "NetworkCleanupConnected",
|
||||
"network_in_use": "NetworkCleanupConnected",
|
||||
"unsupported_network": "NetworkCleanupUnsupported",
|
||||
"already_removed": "NetworkCleanupGone",
|
||||
"inspect_failed": "NetworkCleanupInspectFailed",
|
||||
"remove_failed": "NetworkCleanupRemoveFailed",
|
||||
}[item.Reason]
|
||||
}
|
||||
t.Log(i18n.GetMsgWithMap(key, map[string]interface{}{"name": item.Name, "id": item.ID}))
|
||||
})
|
||||
if report != nil {
|
||||
t.Log(i18n.GetMsgWithMap("NetworkCleanupSummary", map[string]interface{}{
|
||||
"deleted": len(report.Deleted), "skipped": len(report.Skipped), "failed": len(report.Failed),
|
||||
}))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(report.Failed) > 0 {
|
||||
return fmt.Errorf("%s", i18n.GetMsgByKey("NetworkCleanupPartialFailure"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type networkTaskClient struct{ fail bool }
|
||||
|
||||
func (f networkTaskClient) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
|
||||
return []network.Inspect{{ID: "reserved", Name: "1panel-network", Scope: "local"}, {ID: "connected", Name: "busy", Scope: "local"}, {ID: "unused", Name: "free", Scope: "local"}, {ID: "race", Name: "race", Scope: "local"}}, nil
|
||||
}
|
||||
func (f networkTaskClient) ContainerList(context.Context, container.ListOptions) ([]container.Summary, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (f networkTaskClient) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
|
||||
n := network.Inspect{}
|
||||
if id == "connected" {
|
||||
n.Containers = map[string]network.EndpointResource{"container-id": {}}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
func (f networkTaskClient) NetworkRemove(_ context.Context, id string) error {
|
||||
if id == "race" {
|
||||
return errdefs.Conflict(errors.New("has active endpoints"))
|
||||
}
|
||||
if id != "unused" {
|
||||
return errors.New("unexpected removal")
|
||||
}
|
||||
if f.fail {
|
||||
return errors.New("remove failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestNetworkCleanupTaskPersistsLogsAndStatus(t *testing.T) {
|
||||
oldDB, oldTaskDB, oldDir, oldI18n := global.DB, global.TaskDB, global.Dir, global.I18n
|
||||
t.Cleanup(func() { global.DB = oldDB; global.TaskDB = oldTaskDB; global.Dir = oldDir; global.I18n = oldI18n })
|
||||
dir := t.TempDir()
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(dir, "tasks.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
if err := db.AutoMigrate(&model.Task{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
global.DB = nil
|
||||
global.TaskDB = db
|
||||
global.Dir.TaskDir = dir
|
||||
i18n.Init()
|
||||
for _, fail := range []bool{false, true} {
|
||||
name := "success"
|
||||
wantStatus := constant.StatusSuccess
|
||||
if fail {
|
||||
name = "partial failure"
|
||||
wantStatus = constant.StatusFailed
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
item, err := task.NewTaskWithOps("Network", task.TaskClean, task.TaskScopeContainer, "", 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
item.AddSubTask("Clean", func(t *task.Task) error { return executeNetworkCleanup(t, networkTaskClient{fail: fail}) }, nil)
|
||||
err = item.Execute()
|
||||
if (err != nil) != fail {
|
||||
t.Fatalf("unexpected execution error: %v", err)
|
||||
}
|
||||
var saved model.Task
|
||||
if err := db.First(&saved, "id = ?", item.TaskID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.Status != wantStatus {
|
||||
t.Fatalf("status %s, want %s", saved.Status, wantStatus)
|
||||
}
|
||||
content, err := os.ReadFile(saved.LogFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{"[busy] (connected): containers connected", "[race] (race): containers connected", "[1panel-network] (reserved): reserved network", "Network cleanup finished:", "[TASK-END]"} {
|
||||
if !strings.Contains(string(content), want) {
|
||||
t.Fatalf("missing %q in log: %s", want, content)
|
||||
}
|
||||
}
|
||||
want := "Deleted network [free]"
|
||||
if fail {
|
||||
want = "Failed to remove network [free]"
|
||||
}
|
||||
if !strings.Contains(string(content), want) {
|
||||
t.Fatalf("missing %q", want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+11
-72
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -38,7 +37,6 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const sshPath = "/etc/ssh/sshd_config"
|
||||
@@ -440,33 +438,15 @@ func (u *SSHService) SyncRootCert() error {
|
||||
global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err)
|
||||
continue
|
||||
}
|
||||
publicKey, _, _, rest, err := ssh.ParseAuthorizedKey(pubItem)
|
||||
if err != nil || len(bytes.TrimSpace(rest)) != 0 {
|
||||
continue
|
||||
}
|
||||
cert.EncryptionMode = loadSSHKeyType(publicKey)
|
||||
if cert.EncryptionMode == "" {
|
||||
continue
|
||||
}
|
||||
cert.EncryptionMode = loadEncryptioMode(string(pubItem))
|
||||
rootCerts = append(rootCerts, cert)
|
||||
}
|
||||
return hostRepo.SyncCert(rootCerts)
|
||||
}
|
||||
|
||||
func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
|
||||
switch req.Mode {
|
||||
case "generate":
|
||||
switch req.EncryptionMode {
|
||||
case "rsa", "ed25519", "ecdsa", "dsa":
|
||||
default:
|
||||
return buserr.WithName("ErrNotSupportType", req.EncryptionMode)
|
||||
}
|
||||
case "input", "import":
|
||||
if err := validateSSHKeyPair(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return buserr.WithName("ErrNotSupportType", req.Mode)
|
||||
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name))
|
||||
if certItem.ID != 0 {
|
||||
@@ -563,9 +543,6 @@ func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
|
||||
}
|
||||
|
||||
func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
|
||||
if err := validateSSHKeyPair(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load current user failed, err: %v", err)
|
||||
@@ -1704,60 +1681,22 @@ func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
|
||||
return itemDate
|
||||
}
|
||||
|
||||
func loadSSHKeyType(publicKey ssh.PublicKey) string {
|
||||
switch publicKey.Type() {
|
||||
case ssh.KeyAlgoRSA:
|
||||
func loadEncryptioMode(content string) string {
|
||||
if strings.HasPrefix(content, "ssh-rsa") {
|
||||
return "rsa"
|
||||
case ssh.KeyAlgoED25519:
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-ed25519") {
|
||||
return "ed25519"
|
||||
case ssh.KeyAlgoECDSA256, ssh.KeyAlgoECDSA384, ssh.KeyAlgoECDSA521:
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-ecdsa") {
|
||||
return "ecdsa"
|
||||
case ssh.KeyAlgoDSA:
|
||||
}
|
||||
if strings.HasPrefix(content, "ssh-dsa") {
|
||||
return "dsa"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateSSHKeyPair(req *dto.RootCertOperate) error {
|
||||
req.PublicKey = strings.TrimSpace(req.PublicKey)
|
||||
req.PrivateKey = strings.TrimSpace(req.PrivateKey)
|
||||
if req.PublicKey == "" || strings.ContainsAny(req.PublicKey, "\r\n") {
|
||||
return buserr.New("ErrSSHPublicKey")
|
||||
}
|
||||
publicKey, comment, options, rest, err := ssh.ParseAuthorizedKey([]byte(req.PublicKey))
|
||||
if err != nil || len(options) != 0 || len(bytes.TrimSpace(rest)) != 0 {
|
||||
return buserr.New("ErrSSHPublicKey")
|
||||
}
|
||||
req.EncryptionMode = loadSSHKeyType(publicKey)
|
||||
if req.EncryptionMode == "" {
|
||||
return buserr.WithName("ErrNotSupportType", publicKey.Type())
|
||||
}
|
||||
block, rest := pem.Decode([]byte(req.PrivateKey))
|
||||
if !strings.HasPrefix(req.PrivateKey, "-----BEGIN ") || block == nil || len(bytes.TrimSpace(rest)) != 0 {
|
||||
return buserr.New("ErrSSHPrivateKey")
|
||||
}
|
||||
signer, err := ssh.ParsePrivateKey([]byte(req.PrivateKey))
|
||||
var missingPassphrase *ssh.PassphraseMissingError
|
||||
if errors.As(err, &missingPassphrase) {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(req.PrivateKey), []byte(req.PassPhrase))
|
||||
} else if err == nil {
|
||||
req.PassPhrase = ""
|
||||
}
|
||||
if err != nil {
|
||||
return buserr.New("ErrSSHPrivateKey")
|
||||
}
|
||||
if !bytes.Equal(signer.PublicKey().Marshal(), publicKey.Marshal()) {
|
||||
return buserr.New("ErrSSHKeyMismatch")
|
||||
}
|
||||
req.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey)))
|
||||
if comment != "" {
|
||||
req.PublicKey += " " + comment
|
||||
}
|
||||
req.PublicKey += "\n"
|
||||
req.PrivateKey += "\n"
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateLocalConn(newPort uint) error {
|
||||
conn, _ := settingRepo.GetValueByKey("LocalSSHConn")
|
||||
if len(conn) == 0 {
|
||||
|
||||
@@ -154,7 +154,7 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
|
||||
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
|
||||
logger := logrus.New()
|
||||
logger.SetFormatter(&SimpleFormatter{})
|
||||
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, constant.FilePerm)
|
||||
logFile, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
|
||||
@@ -644,13 +644,6 @@
|
||||
"formatZH": "创建容器网络 name",
|
||||
"formatEN": "create container network [name]"
|
||||
},
|
||||
"/containers/network/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "清理未使用的容器网络",
|
||||
"formatEN": "Clean unused container networks"
|
||||
},
|
||||
"/containers/network/del": {
|
||||
"bodyKeys": [
|
||||
"names"
|
||||
@@ -4046,21 +4039,12 @@
|
||||
},
|
||||
"/runtimes/operate": {
|
||||
"bodyKeys": [
|
||||
"ID"
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "ID",
|
||||
"isList": false,
|
||||
"db": "runtimes",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "操作运行环境 [name]",
|
||||
"formatEN": "Operate runtime [name]"
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "操作运行环境 [id]",
|
||||
"formatEN": "Operate runtime [id]"
|
||||
},
|
||||
"/runtimes/php/config": {
|
||||
"bodyKeys": [
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/compose-spec/compose-go/v2 v2.15.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/docker/cli v29.7.2+incompatible
|
||||
github.com/docker/cli v29.8.1+incompatible
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-connections v0.8.1
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
|
||||
+2
-2
@@ -228,8 +228,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
|
||||
github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.8.1+incompatible h1:qYL1bCp6cRw2SB1xmLlIOPyV171dilw9W2Jew38vy9c=
|
||||
github.com/docker/cli v29.8.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
|
||||
|
||||
@@ -731,17 +731,3 @@ FileTaskRename: 'Target filename: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "The current backend {{ .current }} still contains 1Panel rules. Clean it up before switching to {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 forwarding is disabled. Set net.ipv4.ip_forward=1 before using Docker's firewall backend."
|
||||
ErrFirewallRuleSavedApplyFailed: "The new rule configuration was saved, but applying it to the firewall failed. Retry by synchronizing: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "Enter a valid SSH public key"
|
||||
ErrSSHPrivateKey: "Invalid SSH private key or incorrect passphrase; encrypted keys require the original passphrase"
|
||||
ErrSSHKeyMismatch: "The SSH public and private keys do not match"
|
||||
|
||||
NetworkCleanupDeleted: "Deleted network [{{ .name }}] ({{ .id }})"
|
||||
NetworkCleanupProtected: "Skipped network [{{ .name }}] ({{ .id }}): reserved network"
|
||||
NetworkCleanupConnected: "Skipped network [{{ .name }}] ({{ .id }}): containers connected or network in use; not deleted"
|
||||
NetworkCleanupUnsupported: "Skipped network [{{ .name }}] ({{ .id }}): special network"
|
||||
NetworkCleanupGone: "Skipped network [{{ .name }}] ({{ .id }}): already removed"
|
||||
NetworkCleanupInspectFailed: "Failed to inspect network [{{ .name }}] ({{ .id }}); not deleted"
|
||||
NetworkCleanupRemoveFailed: "Failed to remove network [{{ .name }}] ({{ .id }})"
|
||||
NetworkCleanupSummary: "Network cleanup finished: deleted {{ .deleted }}, skipped {{ .skipped }}, failed {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "Some networks could not be cleaned; see the task log"
|
||||
|
||||
@@ -731,7 +731,3 @@ FileTaskRename: 'Nombre del archivo de destino: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "El backend actual {{ .current }} aún contiene reglas de 1Panel. Elimínelas antes de cambiar a {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "El reenvío IPv4 está desactivado. Configure net.ipv4.ip_forward=1 antes de usar el backend de cortafuegos de Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "Se guardó la configuración de la nueva regla, pero no se pudo aplicar al cortafuegos. Vuelva a intentarlo mediante la sincronización: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "Introduce una clave pública SSH válida"
|
||||
ErrSSHPrivateKey: "Clave privada SSH no válida o contraseña incorrecta; las claves cifradas requieren la contraseña original"
|
||||
ErrSSHKeyMismatch: "Las claves pública y privada SSH no coinciden"
|
||||
|
||||
@@ -732,7 +732,3 @@ FileTaskRename: 'نام فایل مقصد: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "بکاند فعلی {{ .current }} هنوز شامل قوانین 1Panel است. پیش از تغییر به {{ .target }} آنها را پاک کنید."
|
||||
ErrDockerIPv4ForwardingDisabled: "ارسال IPv4 غیرفعال است. پیش از استفاده از بکاند فایروال Docker، مقدار net.ipv4.ip_forward=1 را تنظیم کنید."
|
||||
ErrFirewallRuleSavedApplyFailed: "پیکربندی قانون جدید ذخیره شد، اما اعمال آن در دیوار آتش ناموفق بود. با همگامسازی دوباره تلاش کنید: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "یک کلید عمومی SSH معتبر وارد کنید"
|
||||
ErrSSHPrivateKey: "کلید خصوصی SSH نامعتبر یا عبارت عبور نادرست است؛ کلیدهای رمزگذاریشده به عبارت عبور اصلی نیاز دارند"
|
||||
ErrSSHKeyMismatch: "کلید عمومی و خصوصی SSH مطابقت ندارند"
|
||||
|
||||
@@ -731,7 +731,3 @@ FileTaskRename: '保存先ファイル名:{{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "現在のバックエンド {{ .current }} に 1Panel ルールが残っています。{{ .target }} に切り替える前に削除してください。"
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 転送が無効です。Docker のファイアウォールバックエンドを使用する前に net.ipv4.ip_forward=1 を設定してください。"
|
||||
ErrFirewallRuleSavedApplyFailed: "新しいルール設定は保存されましたが、ファイアウォールへの適用に失敗しました。同期で再試行してください:{{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "有効な SSH 公開鍵を入力してください"
|
||||
ErrSSHPrivateKey: "SSH 秘密鍵の形式が無効か、パスフレーズが間違っています。暗号化された鍵には元のパスフレーズが必要です"
|
||||
ErrSSHKeyMismatch: "SSH 公開鍵と秘密鍵が一致しません"
|
||||
|
||||
@@ -731,7 +731,3 @@ FileTaskRename: '대상 파일 이름: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "현재 백엔드 {{ .current }}에 1Panel 규칙이 남아 있습니다. {{ .target }}로 전환하기 전에 정리하세요."
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 전달이 비활성화되어 있습니다. Docker 방화벽 백엔드를 사용하기 전에 net.ipv4.ip_forward=1을 설정하세요."
|
||||
ErrFirewallRuleSavedApplyFailed: "새 규칙 설정이 저장되었지만 방화벽에 적용하지 못했습니다. 동기화하여 다시 시도하세요: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "유효한 SSH 공개 키를 입력하세요"
|
||||
ErrSSHPrivateKey: "SSH 개인 키 형식이 잘못되었거나 암호가 틀립니다. 암호화된 키에는 원래 암호가 필요합니다"
|
||||
ErrSSHKeyMismatch: "SSH 공개 키와 개인 키가 일치하지 않습니다"
|
||||
|
||||
@@ -722,7 +722,3 @@ FileTaskRename: 'ຊື່ໄຟລ໌ປາຍທາງ: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "ແບັກເອນປັດຈຸບັນ {{ .current }} ຍັງມີກົດຂອງ 1Panel. ກະລຸນາລຶບອອກກ່ອນປ່ຽນໄປ {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "ການສົ່ງຕໍ່ IPv4 ຖືກປິດ. ກະລຸນາຕັ້ງ net.ipv4.ip_forward=1 ກ່ອນໃຊ້ແບັກເອນໄຟວໍຂອງ Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "ບັນທຶກການຕັ້ງຄ່າກົດໃໝ່ແລ້ວ ແຕ່ນຳໃຊ້ກັບໄຟວໍບໍ່ສຳເລັດ. ລອງອີກຄັ້ງດ້ວຍການຊິງຂໍ້ມູນ: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "ກະລຸນາປ້ອນຄີສາທາລະນະ SSH ທີ່ຖືກຕ້ອງ"
|
||||
ErrSSHPrivateKey: "ຄີສ່ວນຕົວ SSH ບໍ່ຖືກຕ້ອງ ຫຼື ລະຫັດຜ່ານຜິດ; ຄີທີ່ເຂົ້າລະຫັດຕ້ອງໃຊ້ລະຫັດຜ່ານເດີມ"
|
||||
ErrSSHKeyMismatch: "ຄີສາທາລະນະ ແລະ ຄີສ່ວນຕົວ SSH ບໍ່ກົງກັນ"
|
||||
|
||||
@@ -731,7 +731,3 @@ FileTaskRename: 'Nama fail sasaran: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "Bahagian belakang semasa {{ .current }} masih mengandungi peraturan 1Panel. Buangkannya sebelum beralih kepada {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "Pemajuan IPv4 dilumpuhkan. Tetapkan net.ipv4.ip_forward=1 sebelum menggunakan bahagian belakang tembok api Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "Konfigurasi peraturan baharu telah disimpan, tetapi gagal digunakan pada tembok api. Cuba lagi melalui penyegerakan: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "Masukkan kunci awam SSH yang sah"
|
||||
ErrSSHPrivateKey: "Kunci peribadi SSH tidak sah atau frasa laluan salah; kunci yang disulitkan memerlukan frasa laluan asal"
|
||||
ErrSSHKeyMismatch: "Kunci awam dan peribadi SSH tidak sepadan"
|
||||
|
||||
@@ -731,7 +731,3 @@ FileTaskRename: 'Nome do arquivo de destino: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "O backend atual {{ .current }} ainda contém regras do 1Panel. Remova-as antes de mudar para {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "O encaminhamento IPv4 está desativado. Defina net.ipv4.ip_forward=1 antes de usar o backend de firewall do Docker."
|
||||
ErrFirewallRuleSavedApplyFailed: "A configuração da nova regra foi salva, mas não pôde ser aplicada ao firewall. Tente novamente por meio da sincronização: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "Informe uma chave pública SSH válida"
|
||||
ErrSSHPrivateKey: "Chave privada SSH inválida ou senha incorreta; chaves criptografadas exigem a senha original"
|
||||
ErrSSHKeyMismatch: "As chaves pública e privada SSH não correspondem"
|
||||
|
||||
@@ -731,7 +731,3 @@ FileTaskRename: 'Имя целевого файла: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "В текущем бэкенде {{ .current }} остались правила 1Panel. Удалите их перед переключением на {{ .target }}."
|
||||
ErrDockerIPv4ForwardingDisabled: "Пересылка IPv4 отключена. Перед использованием бэкенда межсетевого экрана Docker установите net.ipv4.ip_forward=1."
|
||||
ErrFirewallRuleSavedApplyFailed: "Настройки нового правила сохранены, но применить их к межсетевому экрану не удалось. Повторите попытку с помощью синхронизации: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "Введите действительный открытый ключ SSH"
|
||||
ErrSSHPrivateKey: "Недопустимый закрытый ключ SSH или неверная парольная фраза; для зашифрованных ключей требуется исходная парольная фраза"
|
||||
ErrSSHKeyMismatch: "Открытый и закрытый ключи SSH не соответствуют друг другу"
|
||||
|
||||
@@ -731,7 +731,3 @@ FileTaskRename: 'Hedef dosya adı: {{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "Mevcut {{ .current }} arka ucunda hâlâ 1Panel kuralları var. {{ .target }} arka ucuna geçmeden önce bunları temizleyin."
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 yönlendirmesi devre dışı. Docker güvenlik duvarı arka ucunu kullanmadan önce net.ipv4.ip_forward=1 ayarını yapın."
|
||||
ErrFirewallRuleSavedApplyFailed: "Yeni kural yapılandırması kaydedildi, ancak güvenlik duvarına uygulanamadı. Eşitleme yaparak yeniden deneyin: {{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "Geçerli bir SSH açık anahtarı girin"
|
||||
ErrSSHPrivateKey: "SSH özel anahtarı geçersiz veya parola yanlış; şifreli anahtarlar mevcut parolayı gerektirir"
|
||||
ErrSSHKeyMismatch: "SSH açık ve özel anahtarları eşleşmiyor"
|
||||
|
||||
@@ -731,17 +731,3 @@ FileTaskRename: '目標檔名:{{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "目前後端 {{ .current }} 中仍有 1Panel 規則,請先清理後再切換至 {{ .target }}。"
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 轉送尚未啟用,請先設定 net.ipv4.ip_forward=1,再使用 Docker 防火牆後端。"
|
||||
ErrFirewallRuleSavedApplyFailed: "新規則設定已儲存,但套用至防火牆失敗。可透過同步重試:{{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "請輸入有效的 SSH 公鑰"
|
||||
ErrSSHPrivateKey: "SSH 私鑰格式無效或密碼錯誤;加密私鑰需要填寫原密碼"
|
||||
ErrSSHKeyMismatch: "SSH 公鑰與私鑰不相符"
|
||||
|
||||
NetworkCleanupDeleted: "網路 [{{ .name }}] ({{ .id }}) 已刪除"
|
||||
NetworkCleanupProtected: "跳過網路 [{{ .name }}] ({{ .id }}):預設保留網路,未刪除"
|
||||
NetworkCleanupConnected: "跳過網路 [{{ .name }}] ({{ .id }}):仍有容器連接或正在使用,未刪除"
|
||||
NetworkCleanupUnsupported: "跳過網路 [{{ .name }}] ({{ .id }}):特殊網路,未刪除"
|
||||
NetworkCleanupGone: "跳過網路 [{{ .name }}] ({{ .id }}):網路已不存在"
|
||||
NetworkCleanupInspectFailed: "網路 [{{ .name }}] ({{ .id }}) 檢查失敗,未刪除"
|
||||
NetworkCleanupRemoveFailed: "網路 [{{ .name }}] ({{ .id }}) 刪除失敗"
|
||||
NetworkCleanupSummary: "網路清理完成:已刪除 {{ .deleted }},已跳過 {{ .skipped }},失敗 {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "部分網路清理失敗,請查看任務日誌"
|
||||
|
||||
@@ -731,17 +731,3 @@ FileTaskRename: '目标文件名:{{ .name }}'
|
||||
ErrFirewallBackendCleanupRequired: "当前后端 {{ .current }} 中仍有 1Panel 规则,请先清理后再切换到 {{ .target }}。"
|
||||
ErrDockerIPv4ForwardingDisabled: "IPv4 转发未开启,请先设置 net.ipv4.ip_forward=1,再使用 Docker 防火墙后端。"
|
||||
ErrFirewallRuleSavedApplyFailed: "新规则配置已保存,但应用到防火墙失败。可通过同步重试:{{ .detail }}"
|
||||
|
||||
ErrSSHPublicKey: "请输入有效的 SSH 公钥"
|
||||
ErrSSHPrivateKey: "SSH 私钥格式无效或密码错误;加密私钥需要填写原密码"
|
||||
ErrSSHKeyMismatch: "SSH 公钥与私钥不匹配"
|
||||
|
||||
NetworkCleanupDeleted: "网络 [{{ .name }}] ({{ .id }}) 已删除"
|
||||
NetworkCleanupProtected: "跳过网络 [{{ .name }}] ({{ .id }}):默认保留网络,未删除"
|
||||
NetworkCleanupConnected: "跳过网络 [{{ .name }}] ({{ .id }}):仍有容器连接或正在使用,未删除"
|
||||
NetworkCleanupUnsupported: "跳过网络 [{{ .name }}] ({{ .id }}):特殊网络,未删除"
|
||||
NetworkCleanupGone: "跳过网络 [{{ .name }}] ({{ .id }}):网络已不存在"
|
||||
NetworkCleanupInspectFailed: "网络 [{{ .name }}] ({{ .id }}) 检查失败,未删除"
|
||||
NetworkCleanupRemoveFailed: "网络 [{{ .name }}] ({{ .id }}) 删除失败"
|
||||
NetworkCleanupSummary: "网络清理完成:已删除 {{ .deleted }},已跳过 {{ .skipped }},失败 {{ .failed }}"
|
||||
NetworkCleanupPartialFailure: "部分网络清理失败,请查看任务日志"
|
||||
|
||||
@@ -77,7 +77,6 @@ func (s *ContainerRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
|
||||
baRouter.GET("/network", baseApi.ListNetwork)
|
||||
baRouter.POST("/network/del", baseApi.DeleteNetwork)
|
||||
baRouter.POST("/network/clean", baseApi.CleanNetworks)
|
||||
baRouter.POST("/network/search", baseApi.SearchNetwork)
|
||||
baRouter.POST("/network", baseApi.CreateNetwork)
|
||||
baRouter.GET("/volume", baseApi.ListVolume)
|
||||
|
||||
@@ -97,7 +97,7 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
|
||||
}()
|
||||
defer close(done)
|
||||
|
||||
client, err := sftp.NewClient(sshClient, sftp.UseConcurrentWrites(true))
|
||||
client, err := sftp.NewClient(sshClient)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -127,15 +127,7 @@ func (s sftpClient) Upload(ctx context.Context, src, target string) (bool, error
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
// ReadFrom uses the pipelined write path of pkg/sftp. A plain io.Copy would
|
||||
// pick os.File.WriteTo (the source is an *os.File), which issues one
|
||||
// synchronous SFTP WRITE per packet and waits for the reply every time, so
|
||||
// the throughput is bounded by packetSize/RTT.
|
||||
written, err := dstFile.ReadFrom(srcFile)
|
||||
if err != nil {
|
||||
// A failed concurrent write may leave holes in the remote file, so cut
|
||||
// it back to the number of bytes actually written.
|
||||
_ = dstFile.Truncate(written)
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return false, ctxErr
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/errdefs"
|
||||
)
|
||||
|
||||
type NetworkCleanupClient interface {
|
||||
NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error)
|
||||
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
||||
NetworkInspect(context.Context, string, network.InspectOptions) (network.Inspect, error)
|
||||
NetworkRemove(context.Context, string) error
|
||||
}
|
||||
|
||||
func CleanUnusedNetworks(ctx context.Context, cli NetworkCleanupClient, onResult ...func(string, dto.NetworkCleanupItem)) (*dto.NetworkCleanupReport, error) {
|
||||
networks, err := cli.NetworkList(ctx, network.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
used := make(map[string]bool)
|
||||
for _, c := range containers {
|
||||
if c.NetworkSettings == nil {
|
||||
continue
|
||||
}
|
||||
for name, endpoint := range c.NetworkSettings.Networks {
|
||||
used[name] = true
|
||||
if endpoint != nil {
|
||||
used[endpoint.NetworkID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
report := &dto.NetworkCleanupReport{Deleted: []dto.NetworkCleanupItem{}, Skipped: []dto.NetworkCleanupItem{}, Failed: []dto.NetworkCleanupItem{}}
|
||||
record := func(status string, item dto.NetworkCleanupItem) {
|
||||
switch status {
|
||||
case "deleted":
|
||||
report.Deleted = append(report.Deleted, item)
|
||||
case "skipped":
|
||||
report.Skipped = append(report.Skipped, item)
|
||||
case "failed":
|
||||
report.Failed = append(report.Failed, item)
|
||||
}
|
||||
for _, notify := range onResult {
|
||||
if notify != nil {
|
||||
notify(status, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(networks, func(i, j int) bool { return networks[i].Name < networks[j].Name })
|
||||
for _, n := range networks {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return report, err
|
||||
}
|
||||
item := dto.NetworkCleanupItem{ID: n.ID, Name: n.Name}
|
||||
switch {
|
||||
case n.Name == "none" || n.Name == "host" || n.Name == "bridge" || n.Name == "1panel-network":
|
||||
item.Reason = "protected"
|
||||
case n.Scope != "local" || n.Ingress || n.ConfigOnly:
|
||||
item.Reason = "unsupported_network"
|
||||
case used[n.Name] || used[n.ID]:
|
||||
item.Reason = "container_connected"
|
||||
}
|
||||
if item.Reason != "" {
|
||||
record("skipped", item)
|
||||
continue
|
||||
}
|
||||
inspected, err := cli.NetworkInspect(ctx, n.ID, network.InspectOptions{})
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
item.Reason = "already_removed"
|
||||
record("skipped", item)
|
||||
} else {
|
||||
item.Reason = "inspect_failed"
|
||||
record("failed", item)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(inspected.Containers) > 0 {
|
||||
item.Reason = "container_connected"
|
||||
record("skipped", item)
|
||||
continue
|
||||
}
|
||||
if err := cli.NetworkRemove(ctx, n.ID); err != nil {
|
||||
switch {
|
||||
case errdefs.IsNotFound(err):
|
||||
item.Reason = "already_removed"
|
||||
record("skipped", item)
|
||||
case errdefs.IsConflict(err):
|
||||
item.Reason = "network_in_use"
|
||||
record("skipped", item)
|
||||
default:
|
||||
item.Reason = "remove_failed"
|
||||
record("failed", item)
|
||||
}
|
||||
continue
|
||||
}
|
||||
record("deleted", item)
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/errdefs"
|
||||
)
|
||||
|
||||
type cleanupFake struct {
|
||||
networks []network.Inspect
|
||||
containers []container.Summary
|
||||
inspected map[string]network.Inspect
|
||||
inspectErrors, removeErrors map[string]error
|
||||
listError error
|
||||
removed []string
|
||||
}
|
||||
|
||||
func (f *cleanupFake) NetworkList(context.Context, network.ListOptions) ([]network.Inspect, error) {
|
||||
return f.networks, nil
|
||||
}
|
||||
func (f *cleanupFake) ContainerList(_ context.Context, o container.ListOptions) ([]container.Summary, error) {
|
||||
if !o.All {
|
||||
panic("must include stopped containers")
|
||||
}
|
||||
return f.containers, f.listError
|
||||
}
|
||||
func (f *cleanupFake) NetworkInspect(_ context.Context, id string, _ network.InspectOptions) (network.Inspect, error) {
|
||||
return f.inspected[id], f.inspectErrors[id]
|
||||
}
|
||||
func (f *cleanupFake) NetworkRemove(_ context.Context, id string) error {
|
||||
f.removed = append(f.removed, id)
|
||||
return f.removeErrors[id]
|
||||
}
|
||||
|
||||
func TestNetworkCleanupProtectsAndReports(t *testing.T) {
|
||||
f := &cleanupFake{inspected: map[string]network.Inspect{"attached": {Containers: map[string]network.EndpointResource{"container": {}}}}, inspectErrors: map[string]error{"unknown": errors.New("inspect failure")}, removeErrors: map[string]error{"race": errdefs.Conflict(errors.New("has active endpoints")), "gone": errdefs.NotFound(errors.New("gone")), "failed": errors.New("denied")}}
|
||||
for _, name := range []string{"none", "host", "bridge", "1panel-network", "configured", "attached", "stopped", "free", "unknown", "race", "gone", "failed"} {
|
||||
f.networks = append(f.networks, network.Inspect{ID: name, Name: name, Scope: "local"})
|
||||
}
|
||||
// Compose ownership labels do not protect an otherwise unused network.
|
||||
for i := range f.networks {
|
||||
if f.networks[i].Name == "configured" {
|
||||
f.networks[i].Labels = map[string]string{"com.docker.compose.project": "demo", "com.docker.compose.network": "default"}
|
||||
}
|
||||
}
|
||||
f.containers = []container.Summary{{State: "exited", NetworkSettings: &container.NetworkSettingsSummary{Networks: map[string]*network.EndpointSettings{"stopped": {NetworkID: "stopped"}}}}}
|
||||
report, err := CleanUnusedNetworks(context.Background(), f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(f.removed, []string{"configured", "failed", "free", "gone", "race"}) {
|
||||
t.Fatal(f.removed)
|
||||
}
|
||||
if len(report.Deleted) != 2 || report.Deleted[0].Name != "configured" || report.Deleted[1].Name != "free" || len(report.Failed) != 2 || len(report.Skipped) != 8 {
|
||||
t.Fatalf("%+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetworkCleanupDiscoveryFailureDeletesNothing(t *testing.T) {
|
||||
f := &cleanupFake{networks: []network.Inspect{{ID: "free", Name: "free", Scope: "local"}}, listError: errors.New("cannot list containers")}
|
||||
if _, err := CleanUnusedNetworks(context.Background(), f); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if len(f.removed) > 0 {
|
||||
t.Fatal(f.removed)
|
||||
}
|
||||
}
|
||||
@@ -644,13 +644,6 @@
|
||||
"formatZH": "创建容器网络 name",
|
||||
"formatEN": "create container network [name]"
|
||||
},
|
||||
"/containers/network/clean": {
|
||||
"bodyKeys": [],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "清理未使用的容器网络",
|
||||
"formatEN": "Clean unused container networks"
|
||||
},
|
||||
"/containers/network/del": {
|
||||
"bodyKeys": [
|
||||
"names"
|
||||
@@ -4046,21 +4039,12 @@
|
||||
},
|
||||
"/runtimes/operate": {
|
||||
"bodyKeys": [
|
||||
"ID"
|
||||
"id"
|
||||
],
|
||||
"paramKeys": [],
|
||||
"beforeFunctions": [
|
||||
{
|
||||
"input_column": "id",
|
||||
"input_value": "ID",
|
||||
"isList": false,
|
||||
"db": "runtimes",
|
||||
"output_column": "name",
|
||||
"output_value": "name"
|
||||
}
|
||||
],
|
||||
"formatZH": "操作运行环境 [name]",
|
||||
"formatEN": "Operate runtime [name]"
|
||||
"beforeFunctions": [],
|
||||
"formatZH": "操作运行环境 [id]",
|
||||
"formatEN": "Operate runtime [id]"
|
||||
},
|
||||
"/runtimes/php/config": {
|
||||
"bodyKeys": [
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# 批量清理未使用的容器网络
|
||||
|
||||
`POST /api/v2/containers/network/clean`,使用现有 1Panel 认证,不需要请求体。
|
||||
这是异步接口,立即返回 `data.taskID`。实际结果在任务中心和任务日志查看,HTTP 请求结束不会取消任务。
|
||||
|
||||
规则:
|
||||
|
||||
- 永远保留 `none`、`host`、`bridge`、`1panel-network`。
|
||||
- 保留所有容器(包括停止容器)引用的网络,删除前再通过 NetworkInspect 检查连接。
|
||||
- 不检查 Compose 文件或应用配置引用;仅被这些配置引用、没有容器连接的普通网络也会被删除。
|
||||
- 当前仅清理本地普通网络,跳过 Swarm、ingress 和 config-only 网络。
|
||||
- Docker 删除时发现新连接会拒绝删除;不会强制断开容器。
|
||||
|
||||
成功响应示例:
|
||||
|
||||
```json
|
||||
{"code":200,"data":{"taskID":"服务端生成的任务 UUID"}}
|
||||
```
|
||||
|
||||
任务类型为 Container,操作为 TaskClean。网络页“清理网络”按钮调用此接口,拿到 taskID 后立即打开任务日志,并刷新任务中心。
|
||||
每个网络处理完成后写入名称、ID 和结果;有容器连接或 Docker 返回占用冲突时,明确记录“未删除”。最后记录已删除、跳过和失败数量。
|
||||
跳过在用网络属于正常结果;检查/删除失败会将任务标记为失败,已删除的网络不回滚。
|
||||
|
||||
日志调用链:
|
||||
|
||||
- `agent/app/service/container_network_cleanup.go`:NewTaskWithOps 创建任务,AddSubTask 注册清理步骤,Execute 执行任务,t.Log 写逐项日志。
|
||||
- `agent/app/task/task.go`:任务持久化,维护状态,写入开始、结束标记;日志文件为 `<global.Dir.TaskDir>/Container/<taskID>.log`。
|
||||
- `frontend/src/views/container/network/index.vue`:请求成功后调用 openTaskLog(taskID)。
|
||||
- `frontend/src/components/log/task/index.vue`:openWithTaskID 打开日志弹窗。
|
||||
- `frontend/src/components/log/file/index.vue`:通过 `/logs/tasks/read` 按行读取任务日志。
|
||||
|
||||
范围:不检查 Compose 文件或应用配置引用;仅清理本地普通网络。原 `/containers/prune` 接口仍保持原逻辑。
|
||||
测试使用假 Docker 客户端和临时任务数据库,不执行真实网络删除。
|
||||
Generated
+178
-13
@@ -126,6 +126,7 @@
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
@@ -1013,6 +1014,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1029,6 +1031,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1045,6 +1048,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1061,6 +1065,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1077,6 +1082,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1093,6 +1099,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1109,6 +1116,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1125,6 +1133,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1141,6 +1150,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1157,6 +1167,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1173,6 +1184,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1189,6 +1201,7 @@
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1205,6 +1218,7 @@
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1221,6 +1235,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1237,6 +1252,7 @@
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1253,6 +1269,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1269,6 +1286,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1285,6 +1303,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1301,6 +1320,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1317,6 +1337,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1333,6 +1354,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1349,6 +1371,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1365,6 +1388,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1381,6 +1405,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1397,6 +1422,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1413,6 +1439,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1746,6 +1773,18 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
@@ -1974,6 +2013,7 @@
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -2013,6 +2053,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2033,6 +2074,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2053,6 +2095,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2073,6 +2116,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2093,6 +2137,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2113,6 +2158,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2133,6 +2179,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2153,6 +2200,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2173,6 +2221,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2193,6 +2242,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2213,6 +2263,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2233,6 +2284,7 @@
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2253,6 +2305,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2297,6 +2350,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2313,6 +2367,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2329,6 +2384,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2345,6 +2401,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2361,6 +2418,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2377,6 +2435,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2393,6 +2452,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2409,6 +2469,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2425,6 +2486,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2441,6 +2503,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2457,6 +2520,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2473,6 +2537,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2489,6 +2554,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2505,6 +2571,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2911,6 +2978,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
|
||||
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
@@ -2939,11 +3007,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz",
|
||||
"integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==",
|
||||
"devOptional": true,
|
||||
"version": "26.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz",
|
||||
"integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~8.9.0"
|
||||
}
|
||||
@@ -3246,6 +3315,7 @@
|
||||
"integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.68.0",
|
||||
"@typescript-eslint/types": "8.68.0",
|
||||
@@ -3929,6 +3999,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.4.0.tgz",
|
||||
"integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.4.0",
|
||||
@@ -3975,13 +4046,15 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
|
||||
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4238,6 +4311,7 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -4267,6 +4341,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.11.26",
|
||||
"caniuse-lite": "^1.0.30001812",
|
||||
@@ -4607,6 +4682,7 @@
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
|
||||
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
@@ -5301,6 +5377,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
@@ -5693,6 +5770,7 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
@@ -5754,6 +5832,7 @@
|
||||
"integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -5813,6 +5892,7 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@@ -6164,6 +6244,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -6265,6 +6346,7 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -6744,7 +6826,7 @@
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
|
||||
"integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/imurmurhash": {
|
||||
@@ -6817,7 +6899,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -6843,7 +6925,7 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
@@ -6888,6 +6970,7 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -6973,7 +7056,7 @@
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -7002,6 +7085,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
@@ -7049,6 +7133,7 @@
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
@@ -7664,13 +7749,15 @@
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -7874,6 +7961,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.5.0",
|
||||
@@ -8171,6 +8259,7 @@
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -8358,6 +8447,7 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
@@ -8613,6 +8703,7 @@
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -8637,6 +8728,7 @@
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
@@ -8717,6 +8809,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.17",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -8828,6 +8921,7 @@
|
||||
"integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -9196,6 +9290,7 @@
|
||||
"integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.143.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
@@ -9305,8 +9400,9 @@
|
||||
"version": "1.105.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.105.0.tgz",
|
||||
"integrity": "sha512-4EjSuRTyG8M4yRXXY1PT9OUV5zz23xBxJkbIAprohKHqf6HOU4MCpE3U3vAC+iO1gxobBjESNbcj8rYAQZWnyA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^5.0.0",
|
||||
"immutable": "^5.1.5",
|
||||
@@ -9532,6 +9628,29 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-correct": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||
@@ -9866,6 +9985,34 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.44.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/text-extensions": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz",
|
||||
@@ -9947,6 +10094,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -9958,6 +10106,7 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -10022,6 +10171,7 @@
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -10060,7 +10210,7 @@
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unimport": {
|
||||
@@ -10450,6 +10600,7 @@
|
||||
"integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.33.0",
|
||||
"picomatch": "^4.0.5",
|
||||
@@ -10724,6 +10875,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10744,6 +10896,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10764,6 +10917,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10784,6 +10938,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10804,6 +10959,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10824,6 +10980,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10844,6 +11001,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10864,6 +11022,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10884,6 +11043,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10904,6 +11064,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10924,6 +11085,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -10962,6 +11124,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.42.tgz",
|
||||
"integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.42",
|
||||
"@vue/compiler-sfc": "3.5.42",
|
||||
@@ -11015,6 +11178,7 @@
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
@@ -11041,6 +11205,7 @@
|
||||
"integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"eslint-scope": "^8.2.0 || ^9.0.0",
|
||||
|
||||
@@ -3,12 +3,13 @@ import { ResPage, SearchWithPage } from '../interface';
|
||||
import { Container } from '../interface/container';
|
||||
import { TimeoutEnum } from '@/enums/http-enum';
|
||||
|
||||
export const searchContainer = (params: Container.ContainerSearch, currentNode?: string, signal?: AbortSignal) => {
|
||||
return http.postWithConfig<ResPage<Container.ContainerInfo>>(`/containers/search`, params, {
|
||||
timeout: TimeoutEnum.T_40S,
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
signal,
|
||||
});
|
||||
export const searchContainer = (params: Container.ContainerSearch, currentNode?: string) => {
|
||||
return http.post<ResPage<Container.ContainerInfo>>(
|
||||
`/containers/search`,
|
||||
params,
|
||||
TimeoutEnum.T_40S,
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
};
|
||||
export const listContainer = () => {
|
||||
return http.post<Array<Container.ContainerOption>>(`/containers/list`, {});
|
||||
@@ -43,11 +44,12 @@ export const downloadContainerFile = (params: { containerID: string; path: strin
|
||||
timeout: TimeoutEnum.T_40S,
|
||||
});
|
||||
};
|
||||
export const loadContainerStatus = (currentNode?: string, containersOnly = false, signal?: AbortSignal) => {
|
||||
return http.get<Container.ContainerStatus>(`/containers/status`, containersOnly ? { containersOnly: true } : {}, {
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
signal,
|
||||
});
|
||||
export const loadContainerStatus = (currentNode?: string) => {
|
||||
return http.get<Container.ContainerStatus>(
|
||||
`/containers/status`,
|
||||
{},
|
||||
currentNode ? { headers: { CurrentNode: currentNode } } : {},
|
||||
);
|
||||
};
|
||||
export const loadResourceLimit = () => {
|
||||
return http.get<Container.ResourceLimit>(`/containers/limit`);
|
||||
@@ -88,11 +90,12 @@ export const containerItemStats = (containerID: string, currentNode?: string) =>
|
||||
currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
);
|
||||
};
|
||||
export const containerListStats = (currentNode?: string, ids?: string[], signal?: AbortSignal) => {
|
||||
return http.get<Array<Container.ContainerListStats>>(`/containers/list/stats`, ids ? { ids: ids.join(',') } : {}, {
|
||||
headers: currentNode ? { CurrentNode: currentNode } : undefined,
|
||||
signal,
|
||||
});
|
||||
export const containerListStats = (currentNode?: string) => {
|
||||
return http.get<Array<Container.ContainerListStats>>(
|
||||
`/containers/list/stats`,
|
||||
{},
|
||||
currentNode ? { headers: { CurrentNode: currentNode } } : {},
|
||||
);
|
||||
};
|
||||
export const containerStats = (id: string) => {
|
||||
return http.get<Container.ContainerStats>(`/containers/stats/${id}`);
|
||||
@@ -161,9 +164,6 @@ export const searchNetwork = (params: SearchWithPage) => {
|
||||
export const listNetwork = () => {
|
||||
return http.get<Array<Container.Options>>(`/containers/network`);
|
||||
};
|
||||
export const cleanNetworks = () => {
|
||||
return http.post<{ taskID: string }>(`/containers/network/clean`, {});
|
||||
};
|
||||
export const deleteNetwork = (params: Container.BatchDelete) => {
|
||||
return http.post(`/containers/network/del`, params);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
<fu-table
|
||||
v-if="currentViewMode === 'table'"
|
||||
v-bind="$attrs"
|
||||
:data="props.data"
|
||||
ref="tableRef"
|
||||
@select="handleSelect"
|
||||
@selection-change="handleSelectionChange"
|
||||
@@ -176,7 +175,7 @@ import { useResponsivePagination } from './useResponsivePagination';
|
||||
import { useTableSelection } from './useTableSelection';
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs();
|
||||
const { isMobile, openMenuTabs, currentNode } = useGlobalStore();
|
||||
const { isMobile, openMenuTabs } = useGlobalStore();
|
||||
|
||||
defineOptions({ name: 'ComplexTable' });
|
||||
export type DropdownProps = FuTableOperationButton;
|
||||
@@ -185,8 +184,6 @@ type ViewMode = 'table' | 'card';
|
||||
|
||||
const props = defineProps({
|
||||
header: String,
|
||||
data: { type: Array as PropType<any[]>, default: () => [] },
|
||||
selectionContext: { type: [String, Number, Array, Object, Function], default: undefined },
|
||||
paginationConfig: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -242,7 +239,7 @@ const selectionColumn = computed(() =>
|
||||
columnNodes.value.find((column) => (column.props as Record<string, any> | null)?.type === 'selection'),
|
||||
);
|
||||
const tableData = computed(() => {
|
||||
const data = props.data;
|
||||
const data = attrs.data;
|
||||
return Array.isArray(data) ? data : [];
|
||||
});
|
||||
const cardContentStyle = computed(() => {
|
||||
@@ -298,7 +295,7 @@ const {
|
||||
isDisabled: isRightButtonDisabled,
|
||||
click: rightButtonClick,
|
||||
} = useContextMenu(() => props.rightButtons);
|
||||
const getTableData = () => props.data || [];
|
||||
const getTableData = () => (Array.isArray(attrs.data) ? attrs.data : []);
|
||||
const isRowSelectable = (row: any) => {
|
||||
const selectable = (selectionColumn.value?.props as Record<string, any> | null)?.selectable;
|
||||
return typeof selectable === 'function' ? selectable(row) : true;
|
||||
@@ -307,6 +304,7 @@ const {
|
||||
selectedRows,
|
||||
isRowSelected,
|
||||
clearSelects,
|
||||
pruneSelection,
|
||||
toggleSelection,
|
||||
selectRow,
|
||||
syncTableSelection,
|
||||
@@ -315,18 +313,7 @@ const {
|
||||
handleRowClick,
|
||||
handleKeyDown: handleSelectionKeyDown,
|
||||
handleKeyUp,
|
||||
} = useTableSelection(
|
||||
tableRef,
|
||||
getTableData,
|
||||
(rows) => emit('update:selects', rows),
|
||||
isRowSelectable,
|
||||
(row) => {
|
||||
const key = attrs.rowKey ?? attrs['row-key'];
|
||||
if (typeof key === 'function') return row == null ? undefined : key(row);
|
||||
if (typeof key === 'string') return key.split('.').reduce((value, part) => value?.[part], row);
|
||||
return row?.id;
|
||||
},
|
||||
);
|
||||
} = useTableSelection(tableRef, getTableData, (rows) => emit('update:selects', rows), isRowSelectable);
|
||||
const toggleCardSelection = (row: any, selected: boolean) => {
|
||||
selectRow(row, selected);
|
||||
};
|
||||
@@ -459,26 +446,7 @@ watch([currentViewMode, tableData, () => props.syncCardContentHeight], scheduleC
|
||||
flush: 'post',
|
||||
});
|
||||
|
||||
watch([() => props.paginationConfig?.currentPage, () => props.paginationConfig?.pageSize], clearSelects, {
|
||||
flush: 'sync',
|
||||
});
|
||||
|
||||
watch(
|
||||
[
|
||||
currentNode,
|
||||
() =>
|
||||
JSON.stringify(
|
||||
typeof props.selectionContext === 'function' ? props.selectionContext() : props.selectionContext,
|
||||
),
|
||||
],
|
||||
() => {
|
||||
clearSelects();
|
||||
if (props.paginationConfig) {
|
||||
props.paginationConfig.currentPage = 1;
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
watch(tableData, pruneSelection);
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', calcHeight);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { shallowRef, ref, toRaw, watch, type Ref } from 'vue';
|
||||
import { shallowRef, ref, toRaw, type Ref } from 'vue';
|
||||
|
||||
type TableRef = { refElTable?: any } | undefined;
|
||||
|
||||
@@ -7,7 +7,6 @@ export const useTableSelection = (
|
||||
getTableData: () => any[],
|
||||
onSelectionChange: (rows: any[]) => void,
|
||||
isRowSelectable: (row: any) => boolean = () => true,
|
||||
getRowKey: (row: any) => unknown = (row) => row?.id,
|
||||
) => {
|
||||
const selectedRows = shallowRef<any[]>([]);
|
||||
const shiftPressed = ref(false);
|
||||
@@ -16,13 +15,8 @@ export const useTableSelection = (
|
||||
let isSyncingTableSelection = false;
|
||||
let skipNextSelectionChange = false;
|
||||
|
||||
let isRefreshingData = false;
|
||||
const sameRow = (left: any, right: any) => {
|
||||
if (left == null || right == null) return left === right;
|
||||
const leftKey = getRowKey(left);
|
||||
const rightKey = getRowKey(right);
|
||||
return leftKey != null && rightKey != null ? leftKey === rightKey : toRaw(left) === toRaw(right);
|
||||
};
|
||||
// Element Plus can return reactive proxies for rows supplied as plain objects.
|
||||
const sameRow = (left: any, right: any) => toRaw(left) === toRaw(right);
|
||||
const hasRow = (rows: any[], row: any) => rows.some((item) => sameRow(item, row));
|
||||
const isRowSelected = (row: any) => hasRow(selectedRows.value, row);
|
||||
const getTable = () => tableRef.value?.refElTable;
|
||||
@@ -45,12 +39,8 @@ export const useTableSelection = (
|
||||
const tableData = getTableData();
|
||||
const nextRows = selectedRows.value.filter((row) => hasRow(tableData, row) && isRowSelectable(row));
|
||||
const currentRows = table.getSelectionRows();
|
||||
currentRows
|
||||
.filter((row) => !nextRows.some((item) => toRaw(item) === toRaw(row)))
|
||||
.forEach((row) => table.toggleRowSelection(row, false));
|
||||
nextRows
|
||||
.filter((row) => !currentRows.some((item) => toRaw(item) === toRaw(row)))
|
||||
.forEach((row) => table.toggleRowSelection(row, true));
|
||||
currentRows.filter((row) => !hasRow(nextRows, row)).forEach((row) => table.toggleRowSelection(row, false));
|
||||
nextRows.filter((row) => !hasRow(currentRows, row)).forEach((row) => table.toggleRowSelection(row, true));
|
||||
} finally {
|
||||
isSyncingTableSelection = false;
|
||||
}
|
||||
@@ -83,7 +73,7 @@ export const useTableSelection = (
|
||||
return true;
|
||||
};
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
if (isSyncingTableSelection || isRefreshingData) {
|
||||
if (isSyncingTableSelection) {
|
||||
return;
|
||||
}
|
||||
if (skipNextSelectionChange) {
|
||||
@@ -114,35 +104,16 @@ export const useTableSelection = (
|
||||
rangeBaseRows.value = [];
|
||||
};
|
||||
const pruneSelection = () => {
|
||||
const data = getTableData();
|
||||
const nextRows = data.filter((row) => hasRow(selectedRows.value, row) && isRowSelectable(row));
|
||||
if (nextRows.length !== selectedRows.value.length || nextRows.some((row, i) => row !== selectedRows.value[i])) {
|
||||
const nextRows = selectedRows.value.filter((row) => hasRow(getTableData(), row));
|
||||
if (nextRows.length !== selectedRows.value.length) {
|
||||
setSelectedRows(nextRows);
|
||||
}
|
||||
lastSelectedRow.value = nextRows.find((row) => sameRow(row, lastSelectedRow.value)) ?? null;
|
||||
rangeBaseRows.value = lastSelectedRow.value ? nextRows.filter((row) => hasRow(rangeBaseRows.value, row)) : [];
|
||||
if (lastSelectedRow.value && !hasRow(nextRows, lastSelectedRow.value)) {
|
||||
lastSelectedRow.value = null;
|
||||
rangeBaseRows.value = [];
|
||||
}
|
||||
syncTableSelection();
|
||||
};
|
||||
// Guard before the child table reacts to replacement data and emits an empty selection.
|
||||
watch(
|
||||
getTableData,
|
||||
() => {
|
||||
isRefreshingData = true;
|
||||
skipNextSelectionChange = false;
|
||||
pruneSelection();
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
watch(
|
||||
getTableData,
|
||||
() => {
|
||||
try {
|
||||
syncTableSelection();
|
||||
} finally {
|
||||
isRefreshingData = false;
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
const toggleSelection = () => {
|
||||
const selectableRows = getTableData().filter(isRowSelectable);
|
||||
const allSelected = selectableRows.length > 0 && selectableRows.every(isRowSelected);
|
||||
@@ -184,6 +155,7 @@ export const useTableSelection = (
|
||||
selectedRows,
|
||||
isRowSelected,
|
||||
clearSelects,
|
||||
pruneSelection,
|
||||
toggleSelection,
|
||||
selectRow,
|
||||
syncTableSelection,
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { watch, type Ref } from 'vue';
|
||||
import GlobalStore from '@/store/modules/global';
|
||||
|
||||
type SearchValue = string | number | undefined;
|
||||
type SearchFields = Record<string, Ref<SearchValue>>;
|
||||
|
||||
export const useSearchPersistence = (page: string, fields: SearchFields, scope?: () => SearchValue) => {
|
||||
const { currentNode } = storeToRefs(GlobalStore());
|
||||
const defaults = Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value]));
|
||||
let initialized = false;
|
||||
let restoring = false;
|
||||
let activeKey: string | undefined;
|
||||
|
||||
watch(
|
||||
() => {
|
||||
const context = scope?.();
|
||||
if (scope && context === undefined) return undefined;
|
||||
return `1panel:search:v1:${JSON.stringify([currentNode.value, page, context ?? ''])}`;
|
||||
},
|
||||
(key) => {
|
||||
activeKey = key;
|
||||
let saved: Record<string, unknown> = {};
|
||||
try {
|
||||
const parsed = activeKey ? JSON.parse(localStorage.getItem(activeKey) || '{}') : {};
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) saved = parsed;
|
||||
} catch {}
|
||||
restoring = true;
|
||||
for (const [name, field] of Object.entries(fields)) {
|
||||
const value = saved[name];
|
||||
const initial = defaults[name];
|
||||
let fallback = initial;
|
||||
if (initialized) {
|
||||
fallback = typeof initial === 'string' ? '' : undefined;
|
||||
}
|
||||
const valid =
|
||||
(typeof value === 'string' || (typeof value === 'number' && Number.isFinite(value))) &&
|
||||
(fallback === undefined || typeof value === typeof fallback);
|
||||
if (value === null) {
|
||||
field.value = typeof initial === 'string' ? '' : undefined;
|
||||
} else if (valid) {
|
||||
field.value = value as SearchValue;
|
||||
} else {
|
||||
field.value = fallback;
|
||||
}
|
||||
}
|
||||
restoring = false;
|
||||
initialized = true;
|
||||
},
|
||||
{ immediate: true, flush: 'sync' },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => Object.fromEntries(Object.entries(fields).map(([name, field]) => [name, field.value ?? null])),
|
||||
(value) => {
|
||||
if (restoring || !activeKey) return;
|
||||
try {
|
||||
localStorage.setItem(activeKey, JSON.stringify(value));
|
||||
} catch {}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
);
|
||||
};
|
||||
@@ -2888,9 +2888,6 @@ const message = {
|
||||
privateKey: 'Private Key',
|
||||
publicKey: 'Public Key',
|
||||
password: 'Password',
|
||||
existingPassPhrase: 'Existing private key passphrase',
|
||||
existingPassPhraseHelper:
|
||||
'Enter the original passphrase if the private key is encrypted; otherwise leave blank. This does not change the passphrase.',
|
||||
createMode: 'Creation Method',
|
||||
generate: 'Auto-generate',
|
||||
unSyncPass: 'Key password cannot be synchronized',
|
||||
|
||||
@@ -2930,9 +2930,6 @@ const message = {
|
||||
privateKey: 'Clave privada',
|
||||
publicKey: 'Clave pública',
|
||||
password: 'Contraseña',
|
||||
existingPassPhrase: 'Contraseña actual de la clave privada',
|
||||
existingPassPhraseHelper:
|
||||
'Introduce la contraseña original si la clave privada está cifrada; en caso contrario, deja el campo vacío. Esto no cambia la contraseña.',
|
||||
createMode: 'Método de creación',
|
||||
generate: 'Autogenerar',
|
||||
unSyncPass: 'La contraseña de la clave no se puede sincronizar',
|
||||
|
||||
@@ -2861,9 +2861,6 @@ const message = {
|
||||
privateKey: 'کلید خصوصی',
|
||||
publicKey: 'کلید عمومی',
|
||||
password: 'رمز عبور',
|
||||
existingPassPhrase: 'عبارت عبور فعلی کلید خصوصی',
|
||||
existingPassPhraseHelper:
|
||||
'اگر کلید خصوصی رمزگذاری شده است، عبارت عبور اصلی را وارد کنید؛ در غیر این صورت خالی بگذارید. این کار عبارت عبور را تغییر نمیدهد.',
|
||||
createMode: 'روش ایجاد',
|
||||
generate: 'تولید خودکار',
|
||||
unSyncPass: 'رمز عبور کلید قابل همگامسازی نیست',
|
||||
|
||||
@@ -2865,9 +2865,6 @@ const message = {
|
||||
privateKey: '秘密鍵',
|
||||
publicKey: '公開鍵',
|
||||
password: 'パスワード',
|
||||
existingPassPhrase: '既存の秘密鍵のパスフレーズ',
|
||||
existingPassPhraseHelper:
|
||||
'秘密鍵が暗号化されている場合は元のパスフレーズを入力し、それ以外は空欄にしてください。パスフレーズは変更されません。',
|
||||
createMode: '作成方法',
|
||||
generate: '自動生成',
|
||||
unSyncPass: '鍵パスワードは同期できません',
|
||||
|
||||
@@ -2827,9 +2827,6 @@ const message = {
|
||||
privateKey: '개인 키',
|
||||
publicKey: '공개 키',
|
||||
password: '비밀번호',
|
||||
existingPassPhrase: '기존 개인 키 암호',
|
||||
existingPassPhraseHelper:
|
||||
'개인 키가 암호화되어 있으면 원래 암호를 입력하고, 그렇지 않으면 비워 두세요. 이 작업은 암호를 변경하지 않습니다.',
|
||||
createMode: '생성 방식',
|
||||
generate: '자동 생성',
|
||||
unSyncPass: '키 비밀번호 동기화 불가',
|
||||
|
||||
@@ -2807,9 +2807,6 @@ const message = {
|
||||
privateKey: 'ຄີສ່ວນຕົວ',
|
||||
publicKey: 'ຄີສາທາລະນະ',
|
||||
password: 'ລະຫັດຜ່ານ',
|
||||
existingPassPhrase: 'ລະຫັດຜ່ານເດີມຂອງຄີສ່ວນຕົວ',
|
||||
existingPassPhraseHelper:
|
||||
'ຖ້າຄີສ່ວນຕົວຖືກເຂົ້າລະຫັດ ໃຫ້ປ້ອນລະຫັດຜ່ານເດີມ; ຖ້າບໍ່ ໃຫ້ປະໄວ້ຫວ່າງ. ການດຳເນີນການນີ້ບໍ່ປ່ຽນລະຫັດຜ່ານ.',
|
||||
createMode: 'ວິທີການສ້າງ',
|
||||
generate: 'ສ້າງອັດຕະໂນມັດ',
|
||||
unSyncPass: 'ບໍ່ສາມາດຊິງຄ໌ລະຫັດຜ່ານຄີໄດ້',
|
||||
|
||||
@@ -2930,9 +2930,6 @@ const message = {
|
||||
privateKey: 'Kunci Persendirian',
|
||||
publicKey: 'Kunci Awam',
|
||||
password: 'Kata Laluan',
|
||||
existingPassPhrase: 'Frasa laluan kunci peribadi sedia ada',
|
||||
existingPassPhraseHelper:
|
||||
'Masukkan frasa laluan asal jika kunci peribadi disulitkan; jika tidak, biarkan kosong. Ini tidak mengubah frasa laluan.',
|
||||
createMode: 'Kaedah Penciptaan',
|
||||
generate: 'Jana Automatik',
|
||||
unSyncPass: 'Kata laluan kunci tidak dapat diselaraskan',
|
||||
|
||||
@@ -2929,9 +2929,6 @@ const message = {
|
||||
privateKey: 'Chave Privada',
|
||||
publicKey: 'Chave Pública',
|
||||
password: 'Senha',
|
||||
existingPassPhrase: 'Senha atual da chave privada',
|
||||
existingPassPhraseHelper:
|
||||
'Informe a senha original se a chave privada estiver criptografada; caso contrário, deixe em branco. Isso não altera a senha.',
|
||||
createMode: 'Método de Criação',
|
||||
generate: 'Gerar Automaticamente',
|
||||
unSyncPass: 'Senha da chave não pode ser sincronizada',
|
||||
|
||||
@@ -2903,9 +2903,6 @@ const message = {
|
||||
privateKey: 'Приватный ключ',
|
||||
publicKey: 'Публичный ключ',
|
||||
password: 'Пароль',
|
||||
existingPassPhrase: 'Текущая парольная фраза закрытого ключа',
|
||||
existingPassPhraseHelper:
|
||||
'Если закрытый ключ зашифрован, введите исходную парольную фразу; иначе оставьте поле пустым. Это не изменяет парольную фразу.',
|
||||
createMode: 'Способ создания',
|
||||
generate: 'Автогенерация',
|
||||
unSyncPass: 'Пароль ключа не может быть синхронизирован',
|
||||
|
||||
@@ -2921,9 +2921,6 @@ const message = {
|
||||
privateKey: 'Özel Anahtar',
|
||||
publicKey: 'Genel Anahtar',
|
||||
password: 'Parola',
|
||||
existingPassPhrase: 'Mevcut özel anahtar parolası',
|
||||
existingPassPhraseHelper:
|
||||
'Özel anahtar şifreliyse mevcut parolayı girin; değilse boş bırakın. Bu işlem parolayı değiştirmez.',
|
||||
createMode: 'Oluşturma Yöntemi',
|
||||
generate: 'Otomatik Oluştur',
|
||||
unSyncPass: 'Anahtar parolası senkronize edilemez',
|
||||
|
||||
@@ -2720,8 +2720,6 @@ const message = {
|
||||
privateKey: '私鑰',
|
||||
publicKey: '公鑰',
|
||||
password: '密碼',
|
||||
existingPassPhrase: '現有私鑰密碼',
|
||||
existingPassPhraseHelper: '私鑰已加密時填寫原密碼,未加密則留空;此處不會修改私鑰密碼。',
|
||||
createMode: '建立方式',
|
||||
generate: '自動生成',
|
||||
unSyncPass: '金鑰密碼無法同步',
|
||||
|
||||
@@ -2751,8 +2751,6 @@ const message = {
|
||||
privateKey: '私钥',
|
||||
publicKey: '公钥',
|
||||
password: '密码',
|
||||
existingPassPhrase: '已有私钥密码',
|
||||
existingPassPhraseHelper: '私钥已加密时填写原密码,未加密则留空;此处不会修改私钥密码。',
|
||||
createMode: '创建方式',
|
||||
generate: '自动生成',
|
||||
unSyncPass: '密钥密码无法同步',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, reactive, toRef } from 'vue';
|
||||
import { ref, reactive } from 'vue';
|
||||
import { MsgError } from '@/utils/message';
|
||||
import { checkStreamAuth } from '@/utils/stream-auth';
|
||||
|
||||
@@ -47,17 +46,6 @@ export const ProcessStore = defineStore('ProcessStore', () => {
|
||||
port: undefined,
|
||||
});
|
||||
|
||||
useSearchPersistence('host/process/process', {
|
||||
pid: toRef(psSearch, 'pid'),
|
||||
name: toRef(psSearch, 'name'),
|
||||
username: toRef(psSearch, 'username'),
|
||||
});
|
||||
useSearchPersistence('host/process/network', {
|
||||
processID: toRef(netSearch, 'processID'),
|
||||
processName: toRef(netSearch, 'processName'),
|
||||
port: toRef(netSearch, 'port'),
|
||||
});
|
||||
|
||||
let pendingRequestType: 'ps' | 'net' | null = null;
|
||||
|
||||
let queuedRequestType: 'ps' | 'net' | null = null;
|
||||
|
||||
@@ -172,7 +172,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Refresh, Search } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
@@ -193,11 +192,6 @@ const searching = ref(false);
|
||||
const operating = ref('');
|
||||
const keyword = ref('');
|
||||
const marketKeyword = ref('');
|
||||
useSearchPersistence(
|
||||
'ai/agents/agent/config/tabs/plugins',
|
||||
{ keyword, marketKeyword },
|
||||
() => agentId.value || undefined,
|
||||
);
|
||||
const origin = ref('');
|
||||
const status = ref('');
|
||||
const page = ref(1);
|
||||
@@ -305,6 +299,8 @@ async function operate(plugin: AI.AgentPluginItem, action: PluginOperate) {
|
||||
async function load(id: number) {
|
||||
agentId.value = id;
|
||||
mode.value = 'installed';
|
||||
keyword.value = '';
|
||||
marketKeyword.value = '';
|
||||
origin.value = '';
|
||||
status.value = '';
|
||||
page.value = 1;
|
||||
|
||||
@@ -138,7 +138,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Refresh, Search } from '@element-plus/icons-vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -169,11 +168,6 @@ const getDefaultMarketSource = (): HermesSkillMarketSource => {
|
||||
const marketSource = ref<HermesSkillMarketSource>(getDefaultMarketSource());
|
||||
const marketSearched = ref(false);
|
||||
const agentId = ref(0);
|
||||
useSearchPersistence(
|
||||
'ai/agents/agent/config/tabs/skills/hermes',
|
||||
{ installedKeyword, marketKeyword },
|
||||
() => agentId.value || undefined,
|
||||
);
|
||||
const installedSkills = ref<AI.AgentSkillItem[]>([]);
|
||||
const marketResults = ref<AI.AgentSkillSearchItem[]>([]);
|
||||
const installingSkill = ref('');
|
||||
@@ -321,6 +315,8 @@ const handleModeChange = async (value: SkillViewMode) => {
|
||||
const load = async (id: number) => {
|
||||
agentId.value = id;
|
||||
mode.value = 'market';
|
||||
installedKeyword.value = '';
|
||||
marketKeyword.value = '';
|
||||
marketSource.value = getDefaultMarketSource();
|
||||
marketSearched.value = false;
|
||||
installedSkills.value = [];
|
||||
|
||||
@@ -142,7 +142,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Refresh, Search } from '@element-plus/icons-vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -181,11 +180,6 @@ const getDefaultMarketSource = (): SkillMarketSource => {
|
||||
const marketSource = ref<SkillMarketSource>(getDefaultMarketSource());
|
||||
const marketSearched = ref(false);
|
||||
const agentId = ref(0);
|
||||
useSearchPersistence(
|
||||
'ai/agents/agent/config/tabs/skills/openclaw',
|
||||
{ installedKeyword, marketKeyword },
|
||||
() => agentId.value || undefined,
|
||||
);
|
||||
const skills = ref<AI.AgentSkillItem[]>([]);
|
||||
const marketResults = ref<AI.AgentSkillSearchItem[]>([]);
|
||||
const updatingSkill = ref('');
|
||||
|
||||
@@ -272,7 +272,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { deleteAgentCheck, pageAgents, resetAgentToken, unbindAgentWebsite, updateAgentRemark } from '@/api/modules/ai';
|
||||
@@ -341,7 +340,6 @@ const isActive = ref(false);
|
||||
const isExist = ref(false);
|
||||
const noApp = ref(false);
|
||||
const searchName = ref('');
|
||||
useSearchPersistence('ai/agents/agent/index', { search: searchName });
|
||||
const defaultHttpsPort = ref(443);
|
||||
const openrestyPortLoaded = ref(false);
|
||||
const websiteDomainsMap = ref<Record<number, Website.Domain[]>>({});
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { deleteAgentAccount, getAgentProviders, pageAgentAccounts } from '@/api/modules/ai';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
@@ -86,7 +85,6 @@ const items = ref<AI.AgentAccountItem[]>([]);
|
||||
const addRef = ref();
|
||||
const modelPoolRef = ref();
|
||||
const searchName = ref('');
|
||||
useSearchPersistence('ai/agents/model/index', { search: searchName });
|
||||
const apiType = ref('');
|
||||
const apiTypeOptions = ref<string[]>([]);
|
||||
|
||||
|
||||
@@ -65,8 +65,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:class="{ mask: maskShow }"
|
||||
@@ -186,7 +184,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import AppStatus from '@/components/app-status/index.vue';
|
||||
import AddDialog from '@/views/ai/model/ollama/add/index.vue';
|
||||
import Conn from '@/views/ai/model/ollama/conn/index.vue';
|
||||
@@ -237,7 +234,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('ai/model/ollama/index', { search: searchName });
|
||||
const appInstallID = ref(0);
|
||||
|
||||
const opRef = ref();
|
||||
|
||||
@@ -70,9 +70,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { App } from '@/api/interface/app';
|
||||
import { toRef, onMounted, reactive, ref } from 'vue';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { searchApp, syncApp, syncCutomAppStore, syncLocalApp, getCurrentNodeCustomAppConfig } from '@/api/modules/app';
|
||||
import Install from '../detail/install/index.vue';
|
||||
import router from '@/routers';
|
||||
@@ -105,7 +104,6 @@ const req = reactive({
|
||||
resource: 'all',
|
||||
showCurrentArch: false,
|
||||
});
|
||||
useSearchPersistence('app-store/apps/index', { search: toRef(req, 'name') });
|
||||
|
||||
const apps = ref<App.AppItem[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
@@ -139,7 +139,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import AppCard from '@/views/app-store/installed/app/card.vue';
|
||||
import Backups from '@/components/backup/index.vue';
|
||||
import Uploads from '@/components/upload/index.vue';
|
||||
@@ -157,7 +156,7 @@ import IgnoreApp from '@/views/app-store/installed/ignore/create/index.vue';
|
||||
import TerminalDialog from '@/views/container/container/terminal/index.vue';
|
||||
|
||||
import { searchAppInstalled, installedOp, appInstalledDeleteCheck, updateAppInstallSort } from '@/api/modules/app';
|
||||
import { toRef, onMounted, onUnmounted, reactive, ref, nextTick } from 'vue';
|
||||
import { onMounted, onUnmounted, reactive, ref, nextTick } from 'vue';
|
||||
import Sortable from 'sortablejs';
|
||||
import i18n from '@/lang';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
@@ -211,7 +210,6 @@ const searchReq = reactive({
|
||||
update: false,
|
||||
sync: false,
|
||||
});
|
||||
useSearchPersistence('app-store/installed/index', { search: toRef(searchReq, 'name') });
|
||||
const router = useRouter();
|
||||
const activeName = ref(i18n.global.t('app.installed'));
|
||||
const mode = ref('installed');
|
||||
|
||||
@@ -521,7 +521,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { computed, h, ref } from 'vue';
|
||||
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
|
||||
import ContainerLog from '@/components/log/container/index.vue';
|
||||
@@ -576,7 +575,6 @@ const dialogBackupRef = ref();
|
||||
const uploadRef = ref();
|
||||
|
||||
const searchName = ref('');
|
||||
useSearchPersistence('container/compose/index', { search: searchName });
|
||||
const includeAppStore = ref(localStorage.getItem('includeAppStore') !== 'false');
|
||||
const showType = ref('compose');
|
||||
const containerStats = ref<any[]>([]);
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, activeTag, includeAppStore, props.filters]"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:view-mode="viewMode"
|
||||
v-model:selects="selects"
|
||||
@@ -474,7 +473,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import PruneDialog from '@/views/container/container/prune/index.vue';
|
||||
import RenameDialog from '@/views/container/container/rename/index.vue';
|
||||
import UpgradeDialog from '@/views/container/container/upgrade/index.vue';
|
||||
@@ -490,7 +488,7 @@ import Uploads from '@/components/upload/index.vue';
|
||||
import DockerStatus from '@/views/container/docker-status/index.vue';
|
||||
import ContainerLogDialog from '@/components/log/container-drawer/index.vue';
|
||||
import Status from '@/components/status/index.vue';
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import {
|
||||
containerItemStats,
|
||||
containerListStats,
|
||||
@@ -536,7 +534,6 @@ const paginationConfig = reactive({
|
||||
order: 'ascending',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('container/container/index', { search: searchName });
|
||||
const dialogUpgradeRef = ref();
|
||||
const dialogCommitRef = ref();
|
||||
const dialogPortJumpRef = ref();
|
||||
@@ -703,87 +700,47 @@ const dialogContainerLogRef = ref();
|
||||
const dialogRenameRef = ref();
|
||||
const dialogPruneRef = ref();
|
||||
|
||||
let requestController: AbortController | undefined;
|
||||
let requestVersion = 0;
|
||||
let refreshPending = false;
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
requestVersion++;
|
||||
requestController?.abort();
|
||||
});
|
||||
|
||||
const loadContainers = async (background = false, column?: any) => {
|
||||
if (!isActive.value || !isExist.value || (background && refreshPending)) {
|
||||
const search = async (column?: any) => {
|
||||
if (!isActive.value || !isExist.value) {
|
||||
return;
|
||||
}
|
||||
requestController?.abort();
|
||||
const controller = new AbortController();
|
||||
requestController = controller;
|
||||
const version = ++requestVersion;
|
||||
const isCurrent = () => version === requestVersion && !controller.signal.aborted;
|
||||
refreshPending = true;
|
||||
loading.value = !background;
|
||||
localStorage.setItem('includeAppStore', includeAppStore.value ? 'true' : 'false');
|
||||
let filterItem = (router.currentRoute.value.query?.filters as string) || '';
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
paginationConfig.order = column?.order ? column.order : paginationConfig.order;
|
||||
const params = {
|
||||
let params = {
|
||||
name: searchName.value,
|
||||
state: paginationConfig.state || 'all',
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
filters: props.filters || (router.currentRoute.value.query?.filters as string) || '',
|
||||
filters: filterItem,
|
||||
orderBy: paginationConfig.orderBy,
|
||||
order: paginationConfig.order,
|
||||
excludeAppStore: !includeAppStore.value,
|
||||
};
|
||||
// Counts and resource samples must never hold up the table itself.
|
||||
const statusRequest = loadContainerStatus(undefined, true, controller.signal)
|
||||
.then((res) => {
|
||||
if (isCurrent()) {
|
||||
updateTags(res.data || {});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
try {
|
||||
const res = await searchContainer(params, undefined, controller.signal);
|
||||
if (!isCurrent()) {
|
||||
return;
|
||||
}
|
||||
const containers = res.data.items || [];
|
||||
loading.value = true;
|
||||
const [containerResult, statsResult, statusResult] = await Promise.allSettled([
|
||||
searchContainer(params),
|
||||
containerListStats(),
|
||||
loadContainerStatus(),
|
||||
]);
|
||||
loading.value = false;
|
||||
|
||||
if (containerResult.status === 'fulfilled') {
|
||||
const containers = containerResult.value.data.items || [];
|
||||
syncContainerRows(containers);
|
||||
paginationConfig.total = res.data.total;
|
||||
loading.value = false;
|
||||
// Clear old usage when a previously running container has stopped.
|
||||
applyStatsToRows(
|
||||
containers
|
||||
.filter((item) => item.state !== 'running')
|
||||
.map((item) => ({
|
||||
containerID: item.containerID,
|
||||
...Object.fromEntries(statFields.map((field) => [field, 0])),
|
||||
})),
|
||||
);
|
||||
const ids = containers.filter((item) => item.state === 'running').map((item) => item.containerID);
|
||||
if (ids.length > 0) {
|
||||
const stats = await containerListStats(undefined, ids, controller.signal);
|
||||
if (isCurrent()) {
|
||||
applyStatsToRows(stats.data || []);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// The HTTP layer reports failures; keep the last successful rows visible.
|
||||
} finally {
|
||||
if (isCurrent()) {
|
||||
loading.value = false;
|
||||
}
|
||||
await statusRequest;
|
||||
if (isCurrent()) {
|
||||
refreshPending = false;
|
||||
}
|
||||
paginationConfig.total = containerResult.value.data.total;
|
||||
}
|
||||
|
||||
if (statsResult.status === 'fulfilled') {
|
||||
applyStatsToRows(statsResult.value.data || []);
|
||||
}
|
||||
|
||||
if (statusResult.status === 'fulfilled') {
|
||||
updateTags(statusResult.value.data || {});
|
||||
}
|
||||
};
|
||||
|
||||
const search = (column?: any) => loadContainers(false, column);
|
||||
|
||||
const searchWithStatus = (item: string) => {
|
||||
activeTag.value = item;
|
||||
paginationConfig.state = activeTag.value;
|
||||
@@ -812,7 +769,21 @@ const changePinned = (row: any, isPinned: boolean) => {
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = () => loadContainers(true);
|
||||
const refresh = async () => {
|
||||
let filterItem = props.filters ? props.filters : '';
|
||||
let params = {
|
||||
name: searchName.value,
|
||||
state: paginationConfig.state || 'all',
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
filters: filterItem,
|
||||
orderBy: paginationConfig.orderBy,
|
||||
order: paginationConfig.order,
|
||||
};
|
||||
const [containerResult, statsResult] = await Promise.all([searchContainer(params), containerListStats()]);
|
||||
syncContainerRows(containerResult.data.items || []);
|
||||
applyStatsToRows(statsResult.data || []);
|
||||
};
|
||||
|
||||
const loadSize = async (row: any) => {
|
||||
containerItemStats(row.containerID).then((res) => {
|
||||
|
||||
@@ -157,8 +157,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { toRef, reactive, ref, computed } from 'vue';
|
||||
import { reactive, ref, computed } from 'vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { computeSize2 } from '@/utils/size';
|
||||
@@ -200,7 +199,6 @@ const paginationConfig = reactive({
|
||||
orderBy: 'createdAt',
|
||||
order: 'null',
|
||||
});
|
||||
useSearchPersistence('container/image/index', { search: toRef(paginationConfig, 'name') });
|
||||
const columns = ref([]);
|
||||
|
||||
const isActive = ref(false);
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
@@ -95,12 +93,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import CreateDialog from '@/views/container/network/create/index.vue';
|
||||
import DetailDrawer from '@/views/container/network/detail/index.vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { deleteNetwork, searchNetwork, inspect, cleanNetworks } from '@/api/modules/container';
|
||||
import { newUUID } from '@/utils/id';
|
||||
import { deleteNetwork, searchNetwork, inspect, containerPrune } from '@/api/modules/container';
|
||||
import { Container } from '@/api/interface/container';
|
||||
import TaskLog from '@/components/log/task/index.vue';
|
||||
import i18n from '@/lang';
|
||||
@@ -120,7 +118,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('container/network/index', { search: searchName });
|
||||
|
||||
const opRef = ref();
|
||||
const isActive = ref(false);
|
||||
@@ -141,10 +138,15 @@ const onClean = () => {
|
||||
type: 'info',
|
||||
}).then(async () => {
|
||||
loading.value = true;
|
||||
await cleanNetworks()
|
||||
.then((res) => {
|
||||
let params = {
|
||||
taskID: newUUID(),
|
||||
pruneType: 'network',
|
||||
withTagAll: false,
|
||||
};
|
||||
await containerPrune(params)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
openTaskLog(res.data.taskID);
|
||||
openTaskLog(params.taskID);
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import OperatorDialog from '@/views/container/repo/operator/index.vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
@@ -78,7 +77,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('container/repo/index', { search: searchName });
|
||||
|
||||
const opRef = ref();
|
||||
const confirmDialog = ref();
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
@@ -81,7 +79,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { dateFormat, getCurrentDateFormatted } from '@/utils/date';
|
||||
import { downloadWithContent } from '@/utils/file';
|
||||
@@ -111,7 +108,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('container/template/index', { search: searchName });
|
||||
|
||||
const search = async () => {
|
||||
if (!isActive.value || !isExist.value) {
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="name"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
@@ -124,7 +122,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import CreateDialog from '@/views/container/volume/create/index.vue';
|
||||
import CodemirrorDrawer from '@/components/codemirror-pro/drawer.vue';
|
||||
import DockerStatus from '@/views/container/docker-status/index.vue';
|
||||
@@ -158,7 +155,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('container/volume/index', { search: searchName });
|
||||
const isActive = ref(false);
|
||||
const isExist = ref(false);
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, searchGroupID]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
:default-sort="
|
||||
paginationConfig.order !== 'null'
|
||||
@@ -272,7 +270,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import Records from '@/views/cronjob/cronjob/record/index.vue';
|
||||
import Backups from '@/views/cronjob/cronjob/backup/index.vue';
|
||||
import Import from '@/views/cronjob/cronjob/import/index.vue';
|
||||
@@ -332,7 +329,6 @@ const pageState = usePageState(() => ({
|
||||
}));
|
||||
const paginationConfig = pageState.paginationConfig;
|
||||
const { defaultGroupID, searchName, searchGroupID } = toRefs(pageState);
|
||||
useSearchPersistence('cronjob/cronjob/index', { search: searchName });
|
||||
|
||||
const search = async (column?: any) => {
|
||||
if (column) {
|
||||
|
||||
@@ -60,8 +60,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchInfo, group]"
|
||||
row-key="id"
|
||||
v-model:selects="selects"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="data"
|
||||
@@ -132,7 +130,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { dateFormat, getCurrentDateFormatted } from '@/utils/date';
|
||||
import { deepCopy } from '@/utils/misc';
|
||||
import { newUUID } from '@/utils/id';
|
||||
@@ -170,7 +167,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const searchInfo = ref<string>('');
|
||||
useSearchPersistence('cronjob/library/index', { search: searchInfo });
|
||||
const group = ref<string>('');
|
||||
const groupOptions = ref();
|
||||
|
||||
|
||||
@@ -323,7 +323,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { nextTick, onMounted, reactive, ref } from 'vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { getRandomStr } from '@/utils/id';
|
||||
@@ -366,7 +365,6 @@ const loading = ref(false);
|
||||
const maskShow = ref(true);
|
||||
const submitLoading = ref(false);
|
||||
const searchName = ref('');
|
||||
useSearchPersistence('database/mongodb/index', { search: searchName });
|
||||
const createVisible = ref(false);
|
||||
const appStatusRef = ref();
|
||||
const bindRef = ref();
|
||||
|
||||
@@ -75,7 +75,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
|
||||
@@ -101,7 +100,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('database/mongodb/remote/index', { search: searchName });
|
||||
|
||||
const search = async (column?: any) => {
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
|
||||
@@ -226,7 +226,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import BindDialog from '@/views/database/mysql/bind/index.vue';
|
||||
import UserDialog from '@/views/database/mysql/user/index.vue';
|
||||
import OperateDialog from '@/views/database/mysql/create/index.vue';
|
||||
@@ -301,7 +300,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('database/mysql/index', { search: searchName });
|
||||
|
||||
const mysqlContainer = ref();
|
||||
const mysqlStatus = ref();
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
|
||||
@@ -100,7 +99,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('database/mysql/remote/index', { search: searchName });
|
||||
|
||||
const search = async (column?: any) => {
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
|
||||
@@ -241,7 +241,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import BindDialog from '@/views/database/postgresql/bind/index.vue';
|
||||
import OperateDialog from '@/views/database/postgresql/create/index.vue';
|
||||
import DeleteDialog from '@/views/database/postgresql/delete/index.vue';
|
||||
@@ -313,7 +312,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('database/postgresql/index', { search: searchName });
|
||||
|
||||
const postgresqlContainer = ref();
|
||||
const postgresqlStatus = ref();
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
|
||||
@@ -100,7 +99,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('database/postgresql/remote/index', { search: searchName });
|
||||
|
||||
const search = async (column?: any) => {
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { deleteCheckDatabase, searchDatabases } from '@/api/modules/database';
|
||||
@@ -99,7 +98,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('database/redis/remote/index', { search: searchName });
|
||||
|
||||
const search = async (column?: any) => {
|
||||
paginationConfig.orderBy = column?.order ? column.prop : paginationConfig.orderBy;
|
||||
|
||||
@@ -739,8 +739,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { toRef, computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
addFavorite,
|
||||
batchGetFileRemarks,
|
||||
@@ -853,7 +852,6 @@ const initData = () => ({
|
||||
sortOrder: 'ascending',
|
||||
});
|
||||
let req = reactive(initData());
|
||||
useSearchPersistence('host/file-management/index', { search: toRef(req, 'search') });
|
||||
let loading = ref(false);
|
||||
const paths = ref<FilePaths[]>([]);
|
||||
const breadcrumbVisiblePaths = ref<FilePaths[]>([]);
|
||||
|
||||
@@ -232,7 +232,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import FireRouter from '@/views/host/firewall/index.vue';
|
||||
import DockerGuardStatus from '@/views/host/firewall/docker/status/index.vue';
|
||||
@@ -275,7 +274,6 @@ const taskLogRef = ref<InstanceType<typeof TaskLog>>();
|
||||
const openRuleTask = (taskID: string) => taskLogRef.value?.openWithTaskID(taskID, true);
|
||||
const orphanDrawerVisible = ref(false);
|
||||
const searchName = ref('');
|
||||
useSearchPersistence('host/firewall/docker/index', { search: searchName });
|
||||
const selects = ref<Firewall.DockerGuardContainer[]>([]);
|
||||
const orphanSelects = ref<Firewall.DockerGuardEndpoint[]>([]);
|
||||
const data = reactive<Firewall.DockerGuardList>({
|
||||
|
||||
@@ -57,18 +57,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, fireName]"
|
||||
:row-key="
|
||||
(row) =>
|
||||
JSON.stringify([
|
||||
row.family,
|
||||
row.protocol,
|
||||
row.port,
|
||||
row.targetIP,
|
||||
row.targetPort,
|
||||
row.interface,
|
||||
])
|
||||
"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
@search="search"
|
||||
@@ -145,7 +133,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import OperateDialog from './operate/index.vue';
|
||||
import ImportDialog from './import/index.vue';
|
||||
import RuleSync from '@/views/host/firewall/sync/index.vue';
|
||||
@@ -166,7 +153,6 @@ import { ElMessageBox } from 'element-plus';
|
||||
const loading = ref();
|
||||
const selects = ref<any>([]);
|
||||
const searchName = ref();
|
||||
useSearchPersistence('host/firewall/forward/index', { search: searchName });
|
||||
|
||||
const isInit = ref(false);
|
||||
const isBind = ref(false);
|
||||
|
||||
@@ -185,7 +185,6 @@
|
||||
<template #main>
|
||||
<div v-loading="loading">
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName, selectedRuleFilters, visibleIptablesChains]"
|
||||
v-model:selects="selects"
|
||||
:pagination-config="paginationConfig"
|
||||
:data="allRows"
|
||||
@@ -398,7 +397,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { Firewall } from '@/api/interface/firewall';
|
||||
import { Process } from '@/api/interface/process';
|
||||
import {
|
||||
@@ -518,7 +516,6 @@ const visibleIptablesChains = ref<string[]>(
|
||||
loadCachedFilterValues(chainFilterStorageKey, iptablesChains, ['1PANEL_BASIC']),
|
||||
);
|
||||
const searchName = ref('');
|
||||
useSearchPersistence('host/firewall/rule/index', { search: searchName });
|
||||
const inventoryItems = ref<Firewall.InventoryItem[]>([]);
|
||||
const positionRanges = ref<Partial<Record<Firewall.Family, Firewall.PositionRange>>>({});
|
||||
const inventoryTotal = ref(0);
|
||||
@@ -662,6 +659,7 @@ const loadRules = async (refreshUsage: boolean) => {
|
||||
paginationConfig.total = total;
|
||||
inventoryTotal.value = response.data.allTotal || 0;
|
||||
managedTotal.value = response.data.managedTotal || 0;
|
||||
selects.value = [];
|
||||
} finally {
|
||||
if (requestID === searchRequestID) loading.value = false;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
|
||||
import { dateFormatServerTimezone } from '@/utils/date';
|
||||
import { downloadFile } from '@/utils/file';
|
||||
@@ -120,7 +119,6 @@ const exportConfig = reactive({
|
||||
status: 'All',
|
||||
});
|
||||
const searchInfo = ref();
|
||||
useSearchPersistence('host/ssh/log/log', { search: searchInfo });
|
||||
const searchStatus = ref('All');
|
||||
|
||||
const search = async () => {
|
||||
|
||||
@@ -24,9 +24,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import FireRouter from '@/views/host/ssh/index.vue';
|
||||
import { toRef, ref, onMounted, onUnmounted, reactive } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, reactive } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { stopProcess } from '@/api/modules/process';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
@@ -38,7 +37,6 @@ const sshSearch = reactive({
|
||||
type: 'ssh',
|
||||
loginUser: '',
|
||||
});
|
||||
useSearchPersistence('host/ssh/session/index', { search: toRef(sshSearch, 'loginUser') });
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
|
||||
@@ -16,15 +16,8 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('ssh.createMode')" prop="mode" v-if="dialogData.title === 'create'">
|
||||
<el-radio-group v-model="dialogData.rowData.mode" @change="onModeChange">
|
||||
<el-radio value="generate">{{ $t('ssh.generate') }}</el-radio>
|
||||
<el-radio value="input">{{ $t('ssh.input') }}</el-radio>
|
||||
<el-radio value="import">{{ $t('ssh.import') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12" v-if="isGenerate">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.encryptionMode')" prop="encryptionMode">
|
||||
<el-select v-model="dialogData.rowData.encryptionMode">
|
||||
<el-option label="ED25519" value="ed25519" />
|
||||
@@ -35,38 +28,50 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t(isGenerate ? 'ssh.password' : 'ssh.existingPassPhrase')" prop="passPhrase">
|
||||
<el-form-item :label="$t('commons.login.password')" prop="passPhrase">
|
||||
<el-input v-model="dialogData.rowData.passPhrase" type="password" show-password>
|
||||
<template #append v-if="isGenerate">
|
||||
<template #append>
|
||||
<el-button @click="random">
|
||||
{{ $t('commons.button.random') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="input-help" v-if="!isGenerate">{{ $t('ssh.existingPassPhraseHelper') }}</span>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div v-if="!isGenerate">
|
||||
<el-form-item :label="$t('ssh.createMode')" prop="mode" v-if="dialogData.title === 'create'">
|
||||
<el-radio-group v-model="dialogData.rowData.mode">
|
||||
<el-radio value="generate">{{ $t('ssh.generate') }}</el-radio>
|
||||
<el-radio value="input">{{ $t('ssh.input') }}</el-radio>
|
||||
<el-radio value="import">{{ $t('ssh.import') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div v-if="dialogData.rowData.mode === 'input'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.privateKey')" prop="privateKey">
|
||||
<el-input type="textarea" :rows="2" v-model="dialogData.rowData.privateKey" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.publicKey')" prop="publicKey">
|
||||
<el-input type="textarea" :rows="2" v-model="dialogData.rowData.publicKey" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div v-if="dialogData.rowData.mode === 'import'">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.privateKey')" prop="privateKey">
|
||||
<el-input
|
||||
v-if="dialogData.rowData.mode === 'input'"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
v-model="dialogData.rowData.privateKey"
|
||||
/>
|
||||
<el-upload
|
||||
v-else
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadPrivateRef"
|
||||
class="upload mt-2 w-full"
|
||||
:limit="1"
|
||||
:on-change="(file) => onKeyChange('privateKey', file)"
|
||||
:on-change="privateOnChange"
|
||||
:on-exceed="privateExceed"
|
||||
:on-remove="() => onKeyRemove('privateKey')"
|
||||
>
|
||||
<el-button size="small" icon="Upload">
|
||||
{{ $t('commons.button.upload') }}
|
||||
@@ -76,22 +81,14 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('ssh.publicKey')" prop="publicKey">
|
||||
<el-input
|
||||
v-if="dialogData.rowData.mode === 'input'"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
v-model="dialogData.rowData.publicKey"
|
||||
/>
|
||||
<el-upload
|
||||
v-else
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
ref="uploadPublicRef"
|
||||
class="upload mt-2 w-full"
|
||||
:limit="1"
|
||||
:on-change="(file) => onKeyChange('publicKey', file)"
|
||||
:on-change="publicOnChange"
|
||||
:on-exceed="publicExceed"
|
||||
:on-remove="() => onKeyRemove('publicKey')"
|
||||
>
|
||||
<el-button size="small" icon="Upload">
|
||||
{{ $t('commons.button.upload') }}
|
||||
@@ -107,7 +104,7 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button @click="drawerVisible = false">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="onConfirm(formRef)">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
@@ -117,7 +114,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, reactive, ref } from 'vue';
|
||||
import { reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { ElForm, genFileId, UploadFile, UploadProps, UploadRawFile } from 'element-plus';
|
||||
import { Host } from '@/api/interface/host';
|
||||
@@ -142,91 +139,37 @@ type FormInstance = InstanceType<typeof ElForm>;
|
||||
const formRef = ref();
|
||||
const uploadPrivateRef = ref();
|
||||
const uploadPublicRef = ref();
|
||||
const isGenerate = computed(() => dialogData.value.title === 'create' && dialogData.value.rowData?.mode === 'generate');
|
||||
|
||||
type KeyField = 'privateKey' | 'publicKey';
|
||||
const keyReaders: Partial<Record<KeyField, FileReader>> = {};
|
||||
|
||||
const cancelKeyRead = (field?: KeyField) => {
|
||||
const fields: KeyField[] = field ? [field] : ['privateKey', 'publicKey'];
|
||||
for (const key of fields) {
|
||||
const reader = keyReaders[key];
|
||||
delete keyReaders[key];
|
||||
reader?.abort();
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => cancelKeyRead());
|
||||
|
||||
const acceptParams = (params: DialogProps): void => {
|
||||
cancelKeyRead();
|
||||
dialogData.value = params;
|
||||
params.rowData.passPhrase ||= '';
|
||||
params.rowData.privateKey ||= '';
|
||||
params.rowData.publicKey ||= '';
|
||||
if (params.title === 'edit') {
|
||||
params.rowData.mode = 'input';
|
||||
dialogData.value.rowData.publicKey = Base64.decode(params.rowData.publicKey);
|
||||
dialogData.value.rowData.privateKey = Base64.decode(params.rowData.privateKey);
|
||||
if (params.rowData.passPhrase) {
|
||||
dialogData.value.rowData.passPhrase = Base64.decode(params.rowData.passPhrase);
|
||||
if (dialogData.value.rowData.passPhrase === '<UN-SET>') {
|
||||
dialogData.value.rowData.passPhrase = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isGenerate.value) {
|
||||
dialogData.value.rowData.encryptionMode = '';
|
||||
}
|
||||
title.value = i18n.global.t('commons.button.' + dialogData.value.title);
|
||||
drawerVisible.value = true;
|
||||
};
|
||||
const emit = defineEmits<{ (e: 'search'): void }>();
|
||||
|
||||
function checkPassword(rule: any, value: any, callback: any) {
|
||||
if (isGenerate.value && value) {
|
||||
if (dialogData.value.rowData.passPhrase !== '') {
|
||||
const reg = /^[A-Za-z0-9]{6,15}$/;
|
||||
if (!reg.test(value)) {
|
||||
if (!reg.test(dialogData.value.rowData.passPhrase)) {
|
||||
return callback(new Error(i18n.global.t('ssh.passwordHelper')));
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
const checkKey = (rule: { field: string }, value: string, callback: (error?: Error) => void) => {
|
||||
if (isGenerate.value) {
|
||||
return callback();
|
||||
}
|
||||
const content = value?.trim();
|
||||
if (!content) {
|
||||
return callback(new Error(i18n.global.t('commons.rule.requiredInput')));
|
||||
}
|
||||
const pattern =
|
||||
rule.field === 'publicKey'
|
||||
? /^(?:ssh-(?:rsa|ed25519|dss)|ecdsa-sha2-nistp(?:256|384|521))[ \t]+[A-Za-z0-9+/]+={0,2}(?:[ \t]+[^\r\n]*)?$/
|
||||
: /^-----BEGIN ((?:OPENSSH |RSA |EC |DSA |ENCRYPTED )?PRIVATE KEY)-----\r?\n(?:Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: [^\r\n]+\r?\n\r?\n)?[A-Za-z0-9+/][A-Za-z0-9+/=\r\n]*\r?\n-----END \1-----$/;
|
||||
if (!pattern.test(content)) {
|
||||
return callback(new Error(i18n.global.t('commons.rule.formatErr')));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
const onModeChange = () => {
|
||||
cancelKeyRead();
|
||||
dialogData.value.rowData.encryptionMode = isGenerate.value ? 'ed25519' : '';
|
||||
dialogData.value.rowData.passPhrase = '';
|
||||
dialogData.value.rowData.privateKey = '';
|
||||
dialogData.value.rowData.publicKey = '';
|
||||
uploadPrivateRef.value?.clearFiles();
|
||||
uploadPublicRef.value?.clearFiles();
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
const rules = reactive({
|
||||
name: Rules.simpleName,
|
||||
encryptionMode: Rules.requiredSelect,
|
||||
passPhrase: [{ validator: checkPassword, trigger: 'blur' }],
|
||||
privateKey: [{ required: true, validator: checkKey, trigger: ['blur', 'change'] }],
|
||||
publicKey: [{ required: true, validator: checkKey, trigger: ['blur', 'change'] }],
|
||||
privateKey: [Rules.requiredInput],
|
||||
publicKey: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const onConfirm = async (formEl: FormInstance | undefined) => {
|
||||
@@ -234,27 +177,23 @@ const onConfirm = async (formEl: FormInstance | undefined) => {
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
const request = {
|
||||
...dialogData.value.rowData,
|
||||
encryptionMode: isGenerate.value ? dialogData.value.rowData.encryptionMode : '',
|
||||
};
|
||||
if (dialogData.value.title === 'create') {
|
||||
await createCert(request)
|
||||
await createCert(dialogData.value.rowData)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
handleClose();
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
} else {
|
||||
await editCert(request)
|
||||
await editCert(dialogData.value.rowData)
|
||||
.then(() => {
|
||||
loading.value = false;
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
handleClose();
|
||||
drawerVisible.value = false;
|
||||
emit('search');
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -264,41 +203,35 @@ const onConfirm = async (formEl: FormInstance | undefined) => {
|
||||
});
|
||||
};
|
||||
|
||||
const onKeyRemove = (field: KeyField) => {
|
||||
cancelKeyRead(field);
|
||||
dialogData.value.rowData[field] = '';
|
||||
formRef.value?.validateField(field).catch(() => {});
|
||||
};
|
||||
|
||||
const onKeyChange = (field: KeyField, uploadFile: UploadFile) => {
|
||||
const rowData = dialogData.value.rowData;
|
||||
if (!drawerVisible.value || rowData.mode !== 'import') return;
|
||||
cancelKeyRead(field);
|
||||
rowData[field] = '';
|
||||
const privateOnChange = (_uploadFile: UploadFile) => {
|
||||
const reader = new FileReader();
|
||||
keyReaders[field] = reader;
|
||||
reader.onload = (e) => {
|
||||
if (keyReaders[field] !== reader || dialogData.value.rowData !== rowData) return;
|
||||
delete keyReaders[field];
|
||||
rowData[field] = e.target.result as string;
|
||||
formRef.value?.validateField(field).catch(() => {});
|
||||
try {
|
||||
dialogData.value.rowData.privateKey = e.target.result as string;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('commons.msg.errImport') + error.message);
|
||||
}
|
||||
};
|
||||
reader.onerror = () => {
|
||||
if (keyReaders[field] !== reader || dialogData.value.rowData !== rowData) return;
|
||||
delete keyReaders[field];
|
||||
MsgError(i18n.global.t('commons.msg.errImport'));
|
||||
};
|
||||
reader.readAsText(uploadFile.raw);
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
const privateExceed: UploadProps['onExceed'] = (files) => {
|
||||
onKeyRemove('privateKey');
|
||||
uploadPrivateRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
uploadPrivateRef.value!.handleStart(file);
|
||||
};
|
||||
const publicOnChange = (_uploadFile: UploadFile) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
dialogData.value.rowData.publicKey = e.target.result as string;
|
||||
} catch (error) {
|
||||
MsgError(i18n.global.t('commons.msg.errImport') + error.message);
|
||||
}
|
||||
};
|
||||
reader.readAsText(_uploadFile.raw);
|
||||
};
|
||||
const publicExceed: UploadProps['onExceed'] = (files) => {
|
||||
onKeyRemove('publicKey');
|
||||
uploadPublicRef.value!.clearFiles();
|
||||
const file = files[0] as UploadRawFile;
|
||||
file.uid = genFileId();
|
||||
@@ -310,7 +243,6 @@ const random = async () => {
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
cancelKeyRead();
|
||||
drawerVisible.value = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||
import { Log } from '@/api/interface/log';
|
||||
import { getSystemLogStatus, listRunningServices, readSystemLogs } from '@/api/modules/log';
|
||||
@@ -114,7 +113,6 @@ const logs = ref<Log.SystemLogItem[]>([]);
|
||||
const loading = ref(false);
|
||||
const watching = ref(false);
|
||||
const keyword = ref('');
|
||||
useSearchPersistence('log/host-system/index', { keyword });
|
||||
const priority = ref('');
|
||||
const service = ref('');
|
||||
const services = ref<string[]>([]);
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
|
||||
import LogRouter from '@/views/log/router/index.vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
@@ -67,7 +66,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const searchInfo = ref<string>('');
|
||||
useSearchPersistence('log/login/index', { search: searchInfo });
|
||||
const searchStatus = ref<string>('');
|
||||
|
||||
const search = async () => {
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
|
||||
import LogRouter from '@/views/log/router/index.vue';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
@@ -146,7 +145,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const searchName = ref<string>('');
|
||||
useSearchPersistence('log/operation/index', { search: searchName });
|
||||
const searchGroup = ref<string>('');
|
||||
const searchStatus = ref<string>('');
|
||||
const searchNode = ref<string>('');
|
||||
|
||||
@@ -120,9 +120,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { dateFormat } from '@/utils/date';
|
||||
import { toRef, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { searchBackup, deleteBackup, refreshToken } from '@/api/modules/backup';
|
||||
import Operate from '@/views/setting/backup-account/operate/index.vue';
|
||||
import DetailShow from '@/components/detail-show/index.vue';
|
||||
@@ -143,7 +142,6 @@ const paginationConfig = reactive({
|
||||
type: '',
|
||||
name: '',
|
||||
});
|
||||
useSearchPersistence('setting/backup-account/index', { search: toRef(paginationConfig, 'name') });
|
||||
const opRef = ref();
|
||||
const dialogRef = ref();
|
||||
const detailRef = ref();
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
v-model:selects="selects"
|
||||
:data="data"
|
||||
@@ -225,7 +223,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import {
|
||||
searchSnapshotPage,
|
||||
snapshotDelete,
|
||||
@@ -261,7 +258,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('setting/snapshot/index', { search: searchName });
|
||||
|
||||
const opRef = ref();
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { Command } from '@/api/interface/command';
|
||||
import GroupDialog from '@/components/group/index.vue';
|
||||
import OperateDialog from '@/views/terminal/command/operate/index.vue';
|
||||
@@ -124,7 +123,6 @@ const paginationConfig = reactive({
|
||||
order: 'ascending',
|
||||
});
|
||||
const info = ref();
|
||||
useSearchPersistence('terminal/command/index', { search: info });
|
||||
const group = ref<string>('');
|
||||
const dialogRef = ref();
|
||||
const opRef = ref();
|
||||
|
||||
@@ -79,7 +79,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import GroupDialog from '@/components/agent-group/index.vue';
|
||||
import OperateDialog from '@/views/terminal/host/operate/index.vue';
|
||||
import { deleteHost, editHostGroup, searchHosts } from '@/api/modules/terminal';
|
||||
@@ -100,7 +99,6 @@ const paginationConfig = reactive({
|
||||
total: 0,
|
||||
});
|
||||
const info = ref();
|
||||
useSearchPersistence('terminal/host/index', { search: info });
|
||||
const group = ref<string>('');
|
||||
|
||||
const opRef = ref();
|
||||
|
||||
@@ -49,8 +49,6 @@
|
||||
</el-card>
|
||||
<template #main v-if="clamStatus.isExist">
|
||||
<ComplexTable
|
||||
:selection-context="() => [searchName]"
|
||||
row-key="id"
|
||||
:class="{ mask: !clamStatus.isRunning }"
|
||||
v-if="!isSettingShow"
|
||||
:pagination-config="paginationConfig"
|
||||
@@ -190,7 +188,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
@@ -218,7 +215,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('toolbox/clam/index', { search: searchName });
|
||||
|
||||
const opRef = ref();
|
||||
const dialogRef = ref();
|
||||
|
||||
@@ -174,7 +174,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
@@ -201,7 +200,6 @@ const paginationConfig = reactive({
|
||||
order: 'null',
|
||||
});
|
||||
const searchName = ref();
|
||||
useSearchPersistence('toolbox/ftp/index', { search: searchName });
|
||||
|
||||
const form = reactive({
|
||||
isActive: true,
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { Runtime } from '@/api/interface/runtime';
|
||||
import { GetPHPExtensions, InstallPHPExtension, UnInstallPHPExtension } from '@/api/modules/runtime';
|
||||
import i18n from '@/lang';
|
||||
@@ -49,7 +48,6 @@ const supportExtensions = ref([]);
|
||||
const loading = ref(false);
|
||||
const taskLogRef = ref();
|
||||
const searchName = ref('');
|
||||
useSearchPersistence('website/runtime/php/extension-management/index', { search: searchName });
|
||||
const data = ref([]);
|
||||
|
||||
const handleClose = () => {
|
||||
@@ -139,6 +137,7 @@ const searchByName = () => {
|
||||
};
|
||||
|
||||
const acceptParams = (req: Runtime.Runtime): void => {
|
||||
searchName.value = '';
|
||||
open.value = true;
|
||||
runtime.value = req;
|
||||
search();
|
||||
|
||||
@@ -221,8 +221,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { toRef, defineAsyncComponent, onMounted, reactive, ref } from 'vue';
|
||||
import { defineAsyncComponent, onMounted, reactive, ref } from 'vue';
|
||||
import { deleteSSL, downloadFile, pushSSLToNode, searchSSL, updateSSL } from '@/api/modules/website';
|
||||
import DnsAccount from './dns-account/index.vue';
|
||||
import AcmeAccount from './acme-account/index.vue';
|
||||
@@ -287,7 +286,6 @@ const req = reactive({
|
||||
orderBy: 'updated_at',
|
||||
order: 'descending',
|
||||
});
|
||||
useSearchPersistence('website/ssl/index', { search: toRef(req, 'domain') });
|
||||
const pushForm = ref({
|
||||
id: 0,
|
||||
pushNode: true,
|
||||
|
||||
@@ -59,7 +59,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { deleteTemplate, searchTemplates } from '@/api/modules/website';
|
||||
import TemplateOperate from '@/views/website/template/operate/index.vue';
|
||||
@@ -72,7 +71,6 @@ import { reactive, ref, onMounted } from 'vue';
|
||||
const loading = ref(false);
|
||||
const data = ref<Website.Template[]>([]);
|
||||
const searchName = ref('');
|
||||
useSearchPersistence('website/template/index', { search: searchName });
|
||||
const opRef = ref();
|
||||
const operateRef = ref();
|
||||
const outputCreateRef = ref();
|
||||
|
||||
@@ -66,8 +66,6 @@
|
||||
</template>
|
||||
<template v-if="!openNginxConfig" #main>
|
||||
<ComplexTable
|
||||
:selection-context="() => [req.name, req.type, req.websiteGroupId]"
|
||||
row-key="id"
|
||||
:pagination-config="paginationConfig"
|
||||
:default-sort="tableSort.order ? tableSort : undefined"
|
||||
v-model:view-mode="viewMode"
|
||||
@@ -346,7 +344,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSearchPersistence } from '@/composables/useSearchPersistence';
|
||||
import Backups from '@/components/backup/index.vue';
|
||||
import UploadDialog from '@/components/upload/index.vue';
|
||||
import DefaultServer from '@/views/website/website/default/index.vue';
|
||||
@@ -361,7 +358,7 @@ import BatchSetGroup from '@/views/website/website/batch-op/group.vue';
|
||||
import BatchSetHttps from '@/views/website/website/batch-op/https.vue';
|
||||
|
||||
import i18n from '@/lang';
|
||||
import { toRef, onMounted, reactive, ref, computed } from 'vue';
|
||||
import { onMounted, reactive, ref, computed } from 'vue';
|
||||
import { batchOperate, opWebsite, searchWebsites, updateWebsite } from '@/api/modules/website';
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { App } from '@/api/interface/app';
|
||||
@@ -451,7 +448,6 @@ const pageState = usePageState(() => ({
|
||||
}));
|
||||
const paginationConfig = pageState.paginationConfig;
|
||||
const req = pageState.req;
|
||||
useSearchPersistence('website/website/index', { search: toRef(req, 'name') });
|
||||
const tableSort = pageState.tableSort;
|
||||
|
||||
const goRouter = async (key: string) => {
|
||||
@@ -500,6 +496,7 @@ const search = async () => {
|
||||
req.pageSize = paginationConfig.pageSize;
|
||||
|
||||
loading.value = true;
|
||||
data.value = [];
|
||||
await searchWebsites(req)
|
||||
.then((res) => {
|
||||
data.value = res.data.items;
|
||||
|
||||
Reference in New Issue
Block a user