Compare commits

..
Author SHA1 Message Date
dependabot[bot] 6565ff6077 chore(deps): bump github.com/docker/cli in /agent
Bumps [github.com/docker/cli](https://github.com/docker/cli) from 29.7.2+incompatible to 29.8.1+incompatible.
- [Commits](https://github.com/docker/cli/compare/v29.7.2...v29.8.1)

---
updated-dependencies:
- dependency-name: github.com/docker/cli
  dependency-version: 29.8.1+incompatible
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-30 08:06:58 +00:00
93 changed files with 401 additions and 1269 deletions
+3 -18
View File
@@ -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)
}
+1 -1
View File
@@ -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 {
-2
View File
@@ -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"`
}
+1 -1
View File
@@ -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"`
+33 -75
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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)
}
+4 -20
View File
@@ -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
View File
@@ -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
View File
@@ -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=
-14
View File
@@ -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"
-4
View File
@@ -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"
-4
View File
@@ -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 مطابقت ندارند"
-4
View File
@@ -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 公開鍵と秘密鍵が一致しません"
-4
View File
@@ -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 공개 키와 개인 키가 일치하지 않습니다"
-4
View File
@@ -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 ບໍ່ກົງກັນ"
-4
View File
@@ -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"
-4
View File
@@ -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"
-4
View File
@@ -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 не соответствуют друг другу"
-4
View File
@@ -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"
-14
View File
@@ -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: "部分網路清理失敗,請查看任務日誌"
-14
View File
@@ -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: "部分网络清理失败,请查看任务日志"
-1
View File
@@ -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)
+2 -10
View File
@@ -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
}
-108
View File
@@ -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)
}
}
+4 -20
View File
@@ -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": [
-33
View File
@@ -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 客户端和临时任务数据库,不执行真实网络删除。
+178 -13
View File
@@ -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",
+19 -19
View File
@@ -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' },
);
};
-3
View File
@@ -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',
-3
View File
@@ -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',
-3
View File
@@ -2861,9 +2861,6 @@ const message = {
privateKey: 'کلید خصوصی',
publicKey: 'کلید عمومی',
password: 'رمز عبور',
existingPassPhrase: 'عبارت عبور فعلی کلید خصوصی',
existingPassPhraseHelper:
'اگر کلید خصوصی رمزگذاری شده است، عبارت عبور اصلی را وارد کنید؛ در غیر این صورت خالی بگذارید. این کار عبارت عبور را تغییر نمی‌دهد.',
createMode: 'روش ایجاد',
generate: 'تولید خودکار',
unSyncPass: 'رمز عبور کلید قابل همگام‌سازی نیست',
-3
View File
@@ -2865,9 +2865,6 @@ const message = {
privateKey: '秘密鍵',
publicKey: '公開鍵',
password: 'パスワード',
existingPassPhrase: '既存の秘密鍵のパスフレーズ',
existingPassPhraseHelper:
'秘密鍵が暗号化されている場合は元のパスフレーズを入力し、それ以外は空欄にしてください。パスフレーズは変更されません。',
createMode: '作成方法',
generate: '自動生成',
unSyncPass: '鍵パスワードは同期できません',
-3
View File
@@ -2827,9 +2827,6 @@ const message = {
privateKey: '개인 키',
publicKey: '공개 키',
password: '비밀번호',
existingPassPhrase: '기존 개인 키 암호',
existingPassPhraseHelper:
'개인 키가 암호화되어 있으면 원래 암호를 입력하고, 그렇지 않으면 비워 두세요. 이 작업은 암호를 변경하지 않습니다.',
createMode: '생성 방식',
generate: '자동 생성',
unSyncPass: '키 비밀번호 동기화 불가',
-3
View File
@@ -2807,9 +2807,6 @@ const message = {
privateKey: 'ຄີສ່ວນຕົວ',
publicKey: 'ຄີສາທາລະນະ',
password: 'ລະຫັດຜ່ານ',
existingPassPhrase: 'ລະຫັດຜ່ານເດີມຂອງຄີສ່ວນຕົວ',
existingPassPhraseHelper:
'ຖ້າຄີສ່ວນຕົວຖືກເຂົ້າລະຫັດ ໃຫ້ປ້ອນລະຫັດຜ່ານເດີມ; ຖ້າບໍ່ ໃຫ້ປະໄວ້ຫວ່າງ. ການດຳເນີນການນີ້ບໍ່ປ່ຽນລະຫັດຜ່ານ.',
createMode: 'ວິທີການສ້າງ',
generate: 'ສ້າງອັດຕະໂນມັດ',
unSyncPass: 'ບໍ່ສາມາດຊິງຄ໌ລະຫັດຜ່ານຄີໄດ້',
-3
View File
@@ -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',
-3
View File
@@ -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',
-3
View File
@@ -2903,9 +2903,6 @@ const message = {
privateKey: 'Приватный ключ',
publicKey: 'Публичный ключ',
password: 'Пароль',
existingPassPhrase: 'Текущая парольная фраза закрытого ключа',
existingPassPhraseHelper:
'Если закрытый ключ зашифрован, введите исходную парольную фразу; иначе оставьте поле пустым. Это не изменяет парольную фразу.',
createMode: 'Способ создания',
generate: 'Автогенерация',
unSyncPass: 'Пароль ключа не может быть синхронизирован',
-3
View File
@@ -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',
-2
View File
@@ -2720,8 +2720,6 @@ const message = {
privateKey: '私鑰',
publicKey: '公鑰',
password: '密碼',
existingPassPhrase: '現有私鑰密碼',
existingPassPhraseHelper: '私鑰已加密時填寫原密碼,未加密則留空;此處不會修改私鑰密碼。',
createMode: '建立方式',
generate: '自動生成',
unSyncPass: '金鑰密碼無法同步',
-2
View File
@@ -2751,8 +2751,6 @@ const message = {
privateKey: '私钥',
publicKey: '公钥',
password: '密码',
existingPassPhrase: '已有私钥密码',
existingPassPhraseHelper: '私钥已加密时填写原密码,未加密则留空;此处不会修改私钥密码。',
createMode: '创建方式',
generate: '自动生成',
unSyncPass: '密钥密码无法同步',
+1 -13
View File
@@ -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();
+1 -3
View File
@@ -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) => {
+1 -3
View File
@@ -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);
+10 -8
View File
@@ -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;
}
-2
View File
@@ -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[]>([]);
-2
View File
@@ -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();
-2
View File
@@ -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();
+1 -3
View File
@@ -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();
+2 -5
View File
@@ -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;